Search code examples
macosparsingapplescriptapple-mail

Parse email for title and date, then create an todo in Things (applescript)


I'm new to Applescript and I've built this from code I've found online, and can't really get it to work.

What I want to do is the following

A rule in Apple Mail will trigger the script to find 2 text strings inside the body of the mail.

I want to extract two things from the e-mail

  1. Due date (Återlämningsdatum)
  2. Title of the book (Titel)

Then I want to create a todo in Things with the title of the book as name of the todo and the due date as due date.

The problem I run into now is that I don't get any data from the mail, just a empty todo is created.

Any ideas?

E-mail below

2017-03-22 18:43:55
MALMÖ STADSBIBLIOTEK
Stadsbiblioteket
Låntagarnummer: **********
Utlån
-------------------------
Titel: Ägg : recept & teknik / Tove Nilsson ; [fotografi: Charlie Drevstam]
Exemplarnummer: 3054550018
Återlämningsdatum: 2017-04-19
-------------------------
Antal utlånade material: 1

Code below

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

set subjectText to "Återlämningsdatum: "
set contentSearch to "Titel: "

using terms from application "Mail"
    on perform mail action with messages theMessages for rule theRule
        tell application "Mail"
            set theContent to content
            set theDate to my getFirstWordAfterSearchText(subjectText, theContent)
            set theTitle to my getFirstWordAfterSearchText(contentSearch, theContent)
        end tell
    end perform mail action with messages
end using terms from

tell application "Things3"
    set newToDo to make new to do
    set name of newToDo to theTitle
    set due date of newToDo to theDate
end tell

(*============== SUBROUTINES =================*)
on getFirstWordAfterSearchText(searchString, theText)
    try
        set {tids, AppleScript's text item delimiters} to {AppleScript's text item delimiters, searchString}
        set textItems to text items of theText
        set AppleScript's text item delimiters to tids
        return (first word of (item 2 of textItems))
    on error theError
        return ""
    end try
end getFirstWordAfterSearchText

Solution

  • In my testing, your script returns the following values:

    theDate: "2017"
    theTitle: "Ägg"
    

    So, I'm assuming you should perhaps get a blank to-do named "Ägg" ? If not, you could try:

        tell application "Things3" to make new to do with properties ¬
            {name: theTitle, due date: theDate}
    

    (although I neither use nor own Things 3, so cannot test this for you).

    Anyway, to address the original problem, the issue is in only asking your handler getFirstWordAfterSearchText to return the first word. I don't speak Swedish, but it looks to me that the title contains more than one word. The date most definitely does, as each word is separated by a hyphen (which is considered a non-word character in AppleScript).

    My suggestion would be to split the email content into paragraphs, isolate the lines of text that start with either "Titel" or "Återlämningsdatum", then return those lines, excluding any unwanted words. Here's a handler that does this:

        to getFirstParagraphThatStarts on searchString ¬
            from theText ¬
            apart from excludedStrings : null
            local searchString, theText, excludedStrings
    
            repeat with P in theText's paragraphs
                if P starts with the searchString then exit repeat
            end repeat
    
            set the text item delimiters to {null} & the excludedStrings
            text items of P as text
        end getFirstParagraphThatStarts
    

    I called the handler much like you did yours:

        set theDate to getFirstParagraphThatStarts on subjectText from theContent
        set theTitle to getFirstParagraphThatStarts on contentSearch from theContent
    

    (I omitted the apart from parameter to begin with, to test the return result before any exclusions were imposed). The return result was this:

    theDate: "Återlämningsdatum: 2017-04-19"
    theTitle: "Titel: Ägg : recept & teknik / Tove Nilsson ; [fotografi: Charlie Drevstam]"
    

    Then I added in the apart from parameter:

        set theDate ¬
            to getFirstParagraphThatStarts on subjectText ¬
            from theContent ¬
            apart from subjectText
    
        set theTitle ¬
            to getFirstParagraphThatStarts on contentSearch ¬
            from theContent ¬
            apart from contentSearch
    

    and the return results were as follows:

    theDate: "2017-04-19"
    theTitle: "Ägg : recept & teknik / Tove Nilsson ; [fotografi: Charlie Drevstam]"
    

    which, I think, is more along the lines of what you are wanting.


    ADDENDUM 1: Dates

    Another thought I had regarding Things 3 is that the due date property might require an actual AppleScript date object, rather than just a string that looks like it might be a date. That is, "2017-04-19" might not be a valid due date. Again, I neither use nor own Things 3, so this is just speculation.

    AppleScript date formats are intrinsically tied to your system settings. As I have my system date/time preferences set to use international ISO-8601 date representations, i.e. yyyy-mm-dd, I can create the AppleScript date object straight from theDate variable like so:

    date theDate
    

    If this is the case with you, and it turns out that you do require a date object, then you can simply set due date of newToDo to date theDate, or (if using my suggested code from above):

        tell application "Things3" to make new to do with properties ¬
            {name: theTitle, due date: date theDate}
    

    If, however, your system settings are set differently, you'll need to construct the AppleScript date object yourself. Here's a handler that will do this:

        to makeASdate out of {year:y, month:m, day:d}
            local y, m, d
    
            tell (the current date) to set ¬
                [ASdate, year, its month, day, time] to ¬
                [it, y, m, d, 0]
    
            ASdate
        end makeASdate
    

    You would then use your theDate variable with this handler like so:

        makeASdate out of {year:word 1, month:word 2, day:word 3} of theDate
            --> date "Wednesday, 19 April 2017 at 0:00:00"
    

    ADDENDUM 2: Scripting Mail (added 2018-08-12)

    After further testing and debugging, you've found that there are other issues with your script that stop it from functioning. This was an oversight on my part, as I ought to have highlighted these errors initially, but became focussed on your handler returning incomplete strings that I neglected to come back to the rest of the script and address its other problems.

    The problem section, as you've discerned, is this one here:

    using terms from application "Mail"
        on perform mail action with messages theMessages for rule theRule
            tell application "Mail"
                set theContent to content
                set theDate to my getFirstWordAfterSearchText(subjectText, theContent)
                set theTitle to my getFirstWordAfterSearchText(contentSearch, theContent)
            end tell
        end perform mail action with messages
    end using terms from
    

    You tell application "Mail" to set theContent to content, however, you haven't specified what the content belongs to (or, rather, implicitly, you've specified that the content belongs to application "Mail", which it doesn't).

    Presumably, you wish to refer to the content of theMessages that are sent through to this script by your mail rules ?

    The other major thing to note is that on perform mail action with messages is an event handler, i.e. it responds to an event taking place in Mail, which causes the handler to be invoked. Defining other handlers elsewhere in the script is perfectly fine; but having stray lines of code that don't belong to any explicit handler will therefore belong to an implicit (hidden) on run handler. This, in a way, is an event handler as well: it responds to a script being run, which is problematic if the script is also being asked to respond to a Mail event simultaneously.

    Therefore, the tell app "Things3" block needs to be moved, and I imagine the variable declarations at the beginning need to be housed as well.

    Bearing all this in mind, I reworked your script to quite a large extent, whilst also trying to keep it resembling your original so it was somewhat recognisable:

        using terms from application "Mail"
            on perform mail action with messages theMessages for rule theRule
                set dateToken to "Återlämningsdatum: "
                set titleToken to "Titel: "
    
                repeat with M in theMessages
                    set theContent to getMessageContent for M
    
                    set datum to restOfLineMatchedAtStart by dateToken from theContent
                    set titel to restOfLineMatchedAtStart by titleToken from theContent
    
                    if false is not in [datum, titel] then
                        set D to makeASdate out of (datum's words as list)
                        createToDoWithDueDate on D given name:titel
                    end if
                end repeat
    
            end perform mail action with messages
        end using terms from
    
    
        to makeASdate out of {Y, M, D}
            local Y, M, D
    
            tell (the current date) to set ¬
                [ASdate, year, its month, day, time] to ¬
                [it, Y, M, D, 0]
    
            ASdate
        end makeASdate
    
    
        to createToDoWithDueDate on D as date given name:N as text
            tell application "Things3" to make new to do ¬
                with properties {name:N, due date:D}
        end createToDoWithDueDate
    
    
        to getMessageContent for msg
            local msg
    
            tell application "Mail" to return msg's content
        end getMessageContent
    
    
        on restOfLineMatchedAtStart by searchString from theText
            local searchString, theText
    
            ignoring white space
                repeat with P in theText's paragraphs
                    if P starts with the searchString then
                        set i to (length of searchString) + (offset of searchString in P)
                        try
                            return text i thru -1 of P
                        on error -- matched entire line
                            return ""
                        end try
                    end if
                end repeat
            end ignoring
    
            false -- no match
        end restOfLineMatchedAtStart
    

    Now, as I don't use Mail and cannot test these mail rules out myself, there could be one or two minor tweaks that you'll discover need to be made before it's running properly. On the other hand, it might run correctly as it is, which would amaze me. But, having spent a lot of time thinking about each line of code, I'm hopeful it's close to what you need.

    The major change you can see is that every piece of code now belongs inside a handler. The custom handlers each get called from inside the main event handler, on perform mail action with messages. I also changed some of the variable names, mainly as part of my mental debugging that was going on (I changed them several times, and have left them to what their last meaningful label was in my head).

    Let me know how it goes. Report back any errors.

    NOTE: Don't forget, however, that this script is designed to be called by the Mail application in response to a mail rule being actioned. Running it from within Script Editor will not do anything.