Send selected emails (Mail / Outlook 2016) to TaskPaper 3

Sends the selected emails, from Apple Mail or Outlook 2016, to an Inbox project in the active TaskPaper 3 document.

To enable outlook:// links from TaskPaper back to the message in Outlook 2016, you will need to:

  1. Download the zip of this Github repository: https://github.com/acidix/OutlookURLHandler
  2. Follow the first three steps in its readme.md, to install the URL handler.

( It isn't code signed, so you may have to refer to this:
https://support.apple.com/kb/PH18657?locale=en_US
but the source code is simple and visible )

(message:// links back to Mail work by default)

KM Macro:
Send selected emails to TaskPaper 3 inbox.kmmacros (26.7 KB)

Source of the JavaScript for Automation action:

// SEND EMAILS SELECTED IN APPLE MAIL OR MS OUTLOOK 2016
// TO INBOX: PROJECT OF FRONT TASKPAPER DOCUMENT

// Ver 0.5 detects whether the front application is Mail or Outlook 2016
// Ver 0.4 Allows for MS Outlook as well as Apple Mail
// Ver 0.31  Keeps whole inbox sorted when new material arrrives,
//          fixes issue when only 1 email was selected
//          checks that new Inbox is created and used if none found
// Ver 0.2  Sorts incoming mail by sender A-Z and datetime received New-Old

(function (dctOptions) {
    'use strict';

    // TASKPAPER CONTEXT

    function TaskPaperContext(editor, options) {

        // FIND OR MAKE A PROJECT TO HOLD INCOMING MAIL
        // String -> String -> {project: tpItem, new: Bool}
        function projectAtPath(strProjectPath, strDefaultName) {
            var strDefault = strDefaultName || 'UnNamed project:',
                outline = editor.outline,
                lstMatch = outline.evaluateItemPath(strProjectPath),
                blnFound = lstMatch.length > 0;

            return {
                project: blnFound ? lstMatch[0] : (function () {
                    var defaultProject = outline.createItem(
                        strDefault
                    );

                    return (
                        outline.groupUndoAndChanges(function () {
                            outline.root.appendChildren(
                                defaultProject
                            );
                        }),
                        defaultProject
                    );

                })(),
                new: !blnFound
            };
        }

        // tpItem -> (tpiItem -> tpItem -> (-1|0|1)) -> tpItem
        function sortChildren(item, fnSort) {
            var lstSort = item.children.sort(fnSort);

            return (
                item.removeChildren(item.children),
                item.insertChildrenBefore(
                    lstSort
                ),
                true
            );
        }
        // FIND OR CREATE AN INBOX

        var outline = editor.outline,
            mInbox = projectAtPath(options.inboxPath, 'Inbox:'),
            itemInbox = mInbox.project,
            lstMsg = options.messages;

        // Doesn't seem to be undoable 
        // Perhaps only mutations of existing material undo ?
        outline.groupUndoAndChanges(function () {
            lstMsg
                .forEach(function (msg) {
                    var item = outline.createItem(
                        '- ' + [msg.sender, msg.subject]
                        .join(' ') +
                        ' @received(' + msg.received + ')'
                    );

                    itemInbox.appendChildren(item);
                    item.appendChildren(
                        outline.createItem(msg.link)
                    );
                });

        });


        if (!mInbox.new && itemInbox.hasChildren) {
            sortChildren(itemInbox, function (a, b) {
                var strRA = a.getAttribute('trailingMatch') || '', // date
                    strRB = a.getAttribute('trailingMatch') || '',

                    strTA = a.bodyString, // all
                    strTB = b.bodyString,

                    iFromA = strTA.charAt(2) === '"' ? 3 : 2,
                    iFromB = strTB.charAt(2) === '"' ? 3 : 2,

                    // Lowercase content, ignoring "- " 
                    // +  any opening doubleQuote
                    strCA = strTA.slice(
                        iFromA, iFromA - strRA.length
                    )
                    .toLowerCase(),
                    strCB = strTB.slice(
                        iFromB, iFromB - strRB.length
                    )
                    .toLowerCase();

                // By A-Z sender and New to Old date
                return strCA === strCB ? (
                    strRA < strRB ? 1 : (strRA > strRB ? -1 : 0)
                ) : (strCA < strCB ? -1 : 1);
            });

            return 'From: ' + options.appName + '\n' + lstMsg
                .map(function (x) {
                    return x.subject;
                })
                .join('\n');
        }
    }


    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // Date -> String
    function fmtTP(dte) {
        var s = dte.toISOString(),
            d = s.substring(0, 10);

        return dte.getMinutes() ? d + ' ' + s.substring(11, 16) : d;
    }

    // String -> String
    function mailURL(strMessageID) {
        return "message://%3C" + strMessageID + "%3E";
    }

    // concatMap :: (a -> [b]) -> [a] -> [b]
    function concatMap(f, xs) {
        return [].concat.apply([], xs.map(f));
    }


    // READ MAIL or OUTLOOK SELECTIONS


    var lstMailApps = ['com.apple.mail', 'com.microsoft.Outlook'],
        procs = Application("System Events")
        .applicationProcesses.whose({
            _match: [ObjectSpecifier()
                .frontmost, true]
        }),
        strAppID = procs.length ? procs[0].bundleIdentifier() : undefined,
        iApp = strAppID ? lstMailApps.indexOf(strAppID) : undefined;

    if ((iApp !== undefined) && (iApp !== -1)) {

        // Key message fields  
        if (strAppID.toLowerCase()
            .indexOf('outlook') !== -1) {

            // MS OUTLOOK 2016
            dctOptions.appName = "Outlook 2016";

            var ol = Application("com.microsoft.Outlook"),
                lstSeln = ol.selectedObjects();

            dctOptions.messages = concatMap(function (x) {
                var strClass = x.class();

                if (strClass
                    .endsWith('Message')) {
                    var dctSender = x.sender();

                    return [{
                        'sender': dctSender.name + ' ' +
                            dctSender.address,
                        'subject': x.subject(),
                        'received': strClass.indexOf(
                                'incoming') ===
                            0 ? fmtTP(x.timeReceived()) : '',
                        'link': 'outlook://' + x.id()
                }]
                } else return [];

            }, lstSeln);

        } else {

            // APPLE MAIL
            dctOptions.appName = "Apple Mail";

            var m = Application("Mail"),
                lstSeln = m.selection();

            dctOptions.messages = lstSeln.length > 0 ? lstSeln
                .map(function (msg) {
                    return {
                        'sender': msg.sender(),
                        'subject': msg.subject(),
                        'received': fmtTP(msg.dateReceived()),
                        'link': mailURL(msg.messageId()),
                    };
                }) : [];
        }

        var ds = Application("com.hogbaysoftware.TaskPaper3")
            .documents,
            d = (ds.length ? ds[0] : undefined);

        if (d) {
            if (d.file()) {
                return d.evaluate({
                    script: TaskPaperContext.toString(),
                    withOptions: dctOptions
                });
            } else {
                var a = Application.currentApplication(),
                    sa = (a.includeStandardAdditions = true, a),
                    strMsg =
                    "Script: (Email to TaskPaper)\n\nFirst save TaskPaper file ...",
                    strAppFolder = sa.pathTo("applications folder", {
                        from: 'user domain'
                    })
                    .toString();

                sa.displayDialog(strMsg, {
                    withTitle: "TaskPaper file not saved"
                });
            }
        }
    } else return strAppID + ' is not a recognised email application'
})({
    inboxPath: '//Inbox and @type=project[-1]'
});
4 Likes

Thanks for sharing this JXA script, Rob.

I'm not so interested in TaskPaper, but I am very interested in both Outlook and JXA, and having a working example of using JXA with OL 2016 is very helpful.

TaskPaper is a very light and useful instrument for planning out what you are about to do and working through it, and it also uses an interesting (and, in particular, unusually fast) scripting architecture.

It’s fast because instead of sending everything back and forth across a relatively sluggish automation interface (the Apple Event approach), it lets you hand over the bulk of the TaskPaper-related JavaScript source (and any data that you need) and runs that JS inside the app itself, in the same space as the application logic. I think we may see more of this.

(See the division, in the source of the macro above, between code for the JavaScript for Automation (Apple Event) context, and code for the TaskPaper (internal) context).

(Taskpaper is also, of course, a good opportunity to experiment with scripting a purely JS environment).

I'm sure it is a great tool. I'm currently using IQTell, which does a great job of:
1. Tight integration with Evernote
2. Project/Task/Action management
3. Integrating your calendar and email with the above

I'm a huge fan/user of Evernote, and IQTell provides the Project/Task/Action management missing from Evernote.

Thank you for sharing code …
I ran into problems when using the generated msgID between two macs running the same outlook account. The same message has different msgID between the two macs. Curious if you ran into similar issues or any thoughts on overcoming this. The messages excchangeId is the same between macs but couldn’t find a mdfind search attribute for exchangeId.

I haven’t tried with multiple macs but I can imagine that issue arising.

What I suggest is that you flag the issue up with the author of the Outlook URL handler scheme

https://github.com/acidix/OutlookURLHandler

and see what thoughts they have, for example about switching to the exchangeID

(PS I notice that in local installations of MS Outlook which are not connected to MS Exchange, the selected objects return missing value for the exchangeId property – so not something that I can text or experiment with here, I’m afraid)

I like IQTell too but it's just a little too expensive to use.

And before you suggest the free version of IQTell, note the following…

  • Sync and Support after 60 days
    requires a paid subscription

I guess it just depends on what you need, and what you value.
For me, $5.83 / month (on the annual plan) is well worth a great project/task management tool, that integrates tightly with Evernote and my email accounts.

That's about 1 beer, or 2 coffees a month. :wink:

If you drink beer and buy your coffee at a designer coffee store. :wink:

If you see the value then by all means go forth young man!

Actually at just about any restaurant, even the cheap ones. It has long amazed me that they charge $2+ for coffee and tea. Even McDonald's charges $1.50 for coffee!

Point understood. To clarify, I make my own coffee at home. But I must say McDonald’s coffee has improved slightly. :slight_smile:

I am having problems with this one.

I select the email (either Mail or Outlook) I run the macro, I get a little notification that says

Selected Email -> TaskPaper 3
/var/folders/2j/andthensomenumbersandletters

But nothing gets created in Taskpaper (3.6). I’m running Sierra and Outlook 2016.

Any help?

Thanks for contributing the macro. I’m having a similar problem - I run the macro, but nothing ends up in Taskpaper (nothing happens at all). I’m using Mail. Is there anything that needs to be done to install this, aside from installing the macro in Keyboard Maestro, and calling the macro through the keystroke?

Editing rights appear to have expired for the original post, since when the Application id of the release version of TaskPaper 3 has changed from that used during the beta stage.

The following copy is updated and is working here:

// SEND EMAILS SELECTED IN APPLE MAIL OR MS OUTLOOK 2016
// TO INBOX: PROJECT OF FRONT TASKPAPER DOCUMENT

// Ver 0.5 detects whether the front application is Mail or Outlook 2016
// Ver 0.4 Allows for MS Outlook as well as Apple Mail
// Ver 0.31  Keeps whole inbox sorted when new material arrrives,
//          fixes issue when only 1 email was selected
//          checks that new Inbox is created and used if none found
// Ver 0.2  Sorts incoming mail by sender A-Z and datetime received New-Old

(function (dctOptions) {
    'use strict';

    // TASKPAPER CONTEXT

    function TaskPaperContext(editor, options) {

        // FIND OR MAKE A PROJECT TO HOLD INCOMING MAIL
        // String -> String -> {project: tpItem, new: Bool}
        function projectAtPath(strProjectPath, strDefaultName) {
            var strDefault = strDefaultName || 'UnNamed project:',
                outline = editor.outline,
                lstMatch = outline.evaluateItemPath(strProjectPath),
                blnFound = lstMatch.length > 0;

            return {
                project: blnFound ? lstMatch[0] : (function () {
                    var defaultProject = outline.createItem(
                        strDefault
                    );

                    return (
                        outline.groupUndoAndChanges(function () {
                            outline.root.appendChildren(
                                defaultProject
                            );
                        }),
                        defaultProject
                    );

                })(),
                new: !blnFound
            };
        }

        // tpItem -> (tpiItem -> tpItem -> (-1|0|1)) -> tpItem
        function sortChildren(item, fnSort) {
            var lstSort = item.children.sort(fnSort);

            return (
                item.removeChildren(item.children),
                item.insertChildrenBefore(
                    lstSort
                ),
                true
            );
        }
        // FIND OR CREATE AN INBOX

        var outline = editor.outline,
            mInbox = projectAtPath(options.inboxPath, 'Inbox:'),
            itemInbox = mInbox.project,
            lstMsg = options.messages;

        // Doesn't seem to be undoable 
        // Perhaps only mutations of existing material undo ?
        outline.groupUndoAndChanges(function () {
            lstMsg
                .forEach(function (msg) {
                    var item = outline.createItem(
                        '- ' + [msg.sender, msg.subject]
                        .join(' ') +
                        ' @received(' + msg.received + ')'
                    );

                    itemInbox.appendChildren(item);
                    item.appendChildren(
                        outline.createItem(msg.link)
                    );
                });

        });


        if (!mInbox.new && itemInbox.hasChildren) {
            sortChildren(itemInbox, function (a, b) {
                var strRA = a.getAttribute('trailingMatch') || '', // date
                    strRB = a.getAttribute('trailingMatch') || '',

                    strTA = a.bodyString, // all
                    strTB = b.bodyString,

                    iFromA = strTA.charAt(2) === '"' ? 3 : 2,
                    iFromB = strTB.charAt(2) === '"' ? 3 : 2,

                    // Lowercase content, ignoring "- " 
                    // +  any opening doubleQuote
                    strCA = strTA.slice(
                        iFromA, iFromA - strRA.length
                    )
                    .toLowerCase(),
                    strCB = strTB.slice(
                        iFromB, iFromB - strRB.length
                    )
                    .toLowerCase();

                // By A-Z sender and New to Old date
                return strCA === strCB ? (
                    strRA < strRB ? 1 : (strRA > strRB ? -1 : 0)
                ) : (strCA < strCB ? -1 : 1);
            });

            return 'From: ' + options.appName + '\n' + lstMsg
                .map(function (x) {
                    return x.subject;
                })
                .join('\n');
        }
    }


    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // Date -> String
    function fmtTP(dte) {
        var s = dte.toISOString(),
            d = s.substring(0, 10);

        return dte.getMinutes() ? d + ' ' + s.substring(11, 16) : d;
    }

    // String -> String
    function mailURL(strMessageID) {
        return "message://%3C" + strMessageID + "%3E";
    }

    // concatMap :: (a -> [b]) -> [a] -> [b]
    function concatMap(f, xs) {
        return [].concat.apply([], xs.map(f));
    }


    // READ MAIL or OUTLOOK SELECTIONS


    var lstMailApps = ['com.apple.mail', 'com.microsoft.Outlook'],
        procs = Application("System Events")
        .applicationProcesses.whose({
            _match: [ObjectSpecifier()
                .frontmost, true]
        }),
        strAppID = procs.length ? procs[0].bundleIdentifier() : undefined,
        iApp = strAppID ? lstMailApps.indexOf(strAppID) : undefined;

    if ((iApp !== undefined) && (iApp !== -1)) {

        // Key message fields  
        if (strAppID.toLowerCase()
            .indexOf('outlook') !== -1) {

            // MS OUTLOOK 2016
            dctOptions.appName = "Outlook 2016";

            var ol = Application("com.microsoft.Outlook"),
                lstSeln = ol.selectedObjects();

            dctOptions.messages = concatMap(function (x) {
                var strClass = x.class();

                if (strClass
                    .endsWith('Message')) {
                    var dctSender = x.sender();

                    return [{
                        'sender': dctSender.name + ' ' +
                            dctSender.address,
                        'subject': x.subject(),
                        'received': strClass.indexOf(
                                'incoming') ===
                            0 ? fmtTP(x.timeReceived()) : '',
                        'link': 'outlook://' + x.id()
                }]
                } else return [];

            }, lstSeln);

        } else {

            // APPLE MAIL
            dctOptions.appName = "Apple Mail";

            var m = Application("Mail"),
                lstSeln = m.selection();

            dctOptions.messages = lstSeln.length > 0 ? lstSeln
                .map(function (msg) {
                    return {
                        'sender': msg.sender(),
                        'subject': msg.subject(),
                        'received': fmtTP(msg.dateReceived()),
                        'link': mailURL(msg.messageId()),
                    };
                }) : [];
        }

        var ds = Application('TaskPaper')
            .documents,
            d = (ds.length ? ds[0] : undefined);

        if (d) {
            if (d.file()) {
                return d.evaluate({
                    script: TaskPaperContext.toString(),
                    withOptions: dctOptions
                });
            } else {
                var a = Application.currentApplication(),
                    sa = (a.includeStandardAdditions = true, a),
                    strMsg =
                    "Script: (Email to TaskPaper)\n\nFirst save TaskPaper file ...",
                    strAppFolder = sa.pathTo("applications folder", {
                        from: 'user domain'
                    })
                    .toString();

                sa.displayDialog(strMsg, {
                    withTitle: "TaskPaper file not saved"
                });
            }
        }
    } else return strAppID + ' is not a recognised email application'
})({
    inboxPath: '//Inbox and @type=project[-1]'
});
2 Likes

That did the trick. Thank you!!

1 Like

So this is my most used Keyboard Maestro Macro. Thank you again. Any idea if it could be tweaked to also add an input box to the workflow?

Example: My voicemail messages come into my Apple Mail inbox. I’d love to fire the macro as I listen to the voicemail message, type the caller’s name (and maybe some notes) as I listen to the message, and have the name go to the start of the task, followed by the rest of the info captured by this macro.

This macro used to work for me, and at some point, it stopped.

Any ideas on how to troubleshoot it?

Also, is there a version without Outlook support? Or is it easy to strip out the Outlook specific code? Since I don’t use Outlook, it occurred to me that might make it easier to troubleshoot.

Might your copy contain an old version of the TaskPaper bundle id ? (There was a change for the release version).

Application("TaskPaper") should work, if that is the case.

Are we talking about line 206? If so, this is what I have there:

		var ds = Application('TaskPaper')