Search code examples
swiftmacoscocoaxcode7quartz-core

How to get e-mail subject from message:// URL in OSX Swift


I have a desktop app that receives e-mail URLs ("message://" scheme) from the drag&drop pasteboard and I want to get the Subject from the relevant message. The only clue I have, so far, is that the QuickLook library might give me an information object where I can retrieve this info from.

Since the QuickLook API seems to be rather in flux at the moment and most examples show how to use it in iOS, I simply cannot find a way to set up my "Preview" object using a URL and get the information from there.

I would like to avoid setting up my project as a QuickLook plugin, or setting up the whole preview pane / view scaffolding; at the moment I just want to get out what QuickLook loads before it starts displaying, but I can't comprehend what paradigm Apple wants me to implement here.

XCode 7.3.1.

Solution

  • It turns out I misinterpreted the contents of draggingInfo.draggingPasteboard().types as a hierarchical list containing only one type of info (URL in this case). Had to subscribe to dragged event type kUTTypeMessage as String and retrieve the e-mail subject from the pasteboard with stringForType("public.url-name")

    EDIT: Note that the current Mail.app will sometimes create a stack of mails when you drag an e-mail thread. Although the method above still works to get the subject of the stack, there is no URL in the dragging info then and since there's no list of Message-IDs available either, I had to resort to scraping the user's mbox directory:

            // See if we can resolve e-mail message meta data
            if let mboxPath = pboard.stringForType("com.apple.mail.PasteboardTypeMessageTransfer") {
                if let automatorPlist = pboard.propertyListForType("com.apple.mail.PasteboardTypeAutomator") {
                    // Get the latest e-mail in the thread
                    if let maxID = (automatorPlist.allObjects.flatMap({ $0["id"]! }) as AnyObject).valueForKeyPath("@max.self") as? Int {
                        // Read its meta data in the background
                        let emailItem = draggingEmailItem
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
                            // Find the e-mail file
                            if let path = Util.findEmlById(searchPath: mboxPath, id: maxID) {
                                // Read its contents
                                emailItem.properties = Util.metaDataFromEml(path)
                                dispatch_async(dispatch_get_main_queue(), {
                                    // Update UI
                                });
                            }
                        }
                    }
                }
            }
    

    Util funcs:

    /* Searches the given path for <id>.eml[x] and returns its URL if found
     */
    static func findEmlById(searchPath searchPath: String, id: Int)-> NSURL? {
        let enumerator = NSFileManager.defaultManager().enumeratorAtPath(searchPath)
        while let element = enumerator?.nextObject() as? NSString {
            switch (element.lastPathComponent, element.pathExtension) {
                case (let lpc, "emlx") where lpc.hasPrefix("\(id)"):
                    return NSURL(fileURLWithPath: searchPath).URLByAppendingPathComponent(element as String)!
                case (let lpc, "eml") where lpc.hasPrefix("\(id)"):
                    return NSURL(fileURLWithPath: searchPath).URLByAppendingPathComponent(element as String)!
                default: ()
            }
        }
        return nil
    }
    
    /* Reads an eml[x] file and parses it, looking for e-mail meta data
     */
    static func metaDataFromEml(path: NSURL)-> Dictionary<String, AnyObject> {
    
        // TODO Support more fields
    
        var properties: Dictionary<String, AnyObject> = [:]
        do {
            let emlxContent = try String(contentsOfURL: path, encoding: NSUTF8StringEncoding)
            // Parse message ID from "...\nMessage-ID: <...>"
            let messageIdStrMatches = emlxContent.regexMatches("[\\n\\r].*Message-ID:\\s*<([^\n\r]*)>")
            if !messageIdStrMatches.isEmpty {
                properties["messageId"] = messageIdStrMatches[0] as String
            }
        }
        catch {
            print("ERROR: Failed to open emlx file")
        }
        return properties
    }
    

    Note: If your app is sandboxed you will need the com.apple.security.temporary-exception.files.home-relative-path.read-only entitlement set to an array with one string in it: /Library/