1

I've created a rule to move mail from specified senders to a new mailbox called "News." I'd like my AppleScript to go through the "News" folder and only keep the most recent emaisa from those senders, deleting prior messages. (This is like one of the Sweep functions on MSLive/Outlook.) I've tried writing a script as follows, which is invoked by the rule:

using terms from application "Mail"
    on perform mail action with messages these_messages for rule this_rule
        tell application "Mail"
            set dateToday to current date
            set TargetInbox to mailbox "News" of account "iCloud"
            set EveryMessage to every message of TargetInbox
            repeat with eachMessage in these_messages
                repeat with this_message in EveryMessage
                    set eachSender to the sender of eachMessage
                    set this_sender to the sender of this_message
                    set messageDate to date received of EveryMessage
                    if messageDate < dateToday and eachSender = this_sender then
                        delete this_message
                    end if
                end repeat
            end repeat
        end tell
    end perform mail action with messages
end using terms from

I see that the script is called by the rule, because of the spinning gear on the menu bar, but none of the desired deleting occurs, and the script seems to hang in some kind of loop.

I'm an AppleScript n00b, do much better with JavaScript. I'd appreciate any help.

Thanks, Betalantz

macos 10.13.5 | Mail 11.4 | AppleScript 2.7

3
  • I will take a look more extensively later. But what I like to do is test every part of a script. For example you can use display dialog "Hi", which is like the JavaScript equivalent of alert("Hi"), in the repeats to check if they are reappearing correctly. Then you switch out Hi with a variable that you use (such as this_message) so you can make sure that the variable is correct. Commented Jun 27, 2018 at 17:07
  • Great tip, to use display dialog as a kind of console.log for debugging. Will give it a try. Commented Jun 27, 2018 at 17:59
  • There is also a console option in AppleScript but I always preferred dialogs. :) Commented Jun 27, 2018 at 18:00

3 Answers 3

3

I don't use Mail.app and haven't much experience implementing Mail actions, so am not in the position to test your script. However, reading through it, there are places in it of potential concern that I would wish to look to first when debugging.

How large is your "News" mailbox ?

There are two features in your script that threaten it to hang or timeout:

1. set EveryMessage to every message of TargetInbox

If that mailbox has thousands or even hundreds of emails in it, retrieving every message as a list of individually referenced message objects (which is what this statement is doing) could take some time. It's almost always way more efficient and less intensive on processor and memory usage to store a reference to the every message object (collection):

set EveryMessage to a reference to every message of the TargetInbox

It has another beneficial feature that I'll address later.

2. Potentially demanding nested repeat loops

Nested loops are a necessary evil sometimes, but the query about the mailbox size is, again, a source of anxiety for me, especially since its called upon in at the heart of the nest where the bulk of the operations are done. This means that, for each new message that triggers the script, every single message in the mailbox has to be checked and compared to the trigger message.

These are two branches of one issue, really, because if your mailbox only has 5 messages in it, none of this is particularly bothersome. Otherwise, you're first getting AppleScript to retrieve every message in the mailbox and store them in a variable; then cycling through this potentially heavy load and comparing each item to another.

If you had no choice but to use a repeat loop whilst knowing that it would have to iterate through a lot of list items (once you exceed about 500 of anything, AppleScript can begin to buckle) it can help, once again, to pass AppleScript a reference to the list of items instead of the actual list. Passing any reference through a script object (which is what a reference to essentially does), is much, much faster than making AppleScript access an object by other means. Here's an excellent explanation and demonstration of these principles.

Simple changes and error corrections

After addressing the heavy issue above, which could turn out to be of little relevance to your particular case, here are one or two de facto elements that are easily changed to either improve the efficiency of the script, or prevent it throwing an error:

1. Placement of variable declarations

set eachSender to the sender of eachMessage

This line is currently declared inside the inner repeat loop, which also happens to be the the meatier of the two, in general hypothetical terms. However, what this is doing is setting the value of the variable eachSender to the same value for as many messages you have in your mailbox. That's a lot of wasted operations for AppleScript (a total of the number of these_messages multplied by the number of EveryMessage).

Instead, move it to here:

repeat with eachMessage in these_messages
    set eachSender to the sender of eachMessage  # ...now it's here
    repeat with this_message in EveryMessage
        # It was here...
        .
        .
        .
    end repeat
end repeat

Now AppleScript only has to set the variable once for eachMessage in these_message. That's an efficiency improvement of polynomial time down to linear time!

2. set messageDate to date received of EveryMessage

Initially, I thought you were trying to access the EveryMessage object as a reference to the collection I spoke of earlier, and simply didn't realise you couldn't do that in the way you declared EveryMessage.

But, I now realise it was most likely just your hands typing faster than your thoughts, and introducing an irksome typo. EveryMessage here ought to bethis_message, i.e.:

set messageDate to date received of this_message

This leads me quite nicely onto my final point, which I hinted at earlier, regarding the other benefit of declaring a variable to an object collection as a reference.

Had you done this:

set EveryMessage to a reference to every message of the TargetInbox

then this:

set messageDate to date received of EveryMessage

would be a perfectly valid piece of AppleScript, which allows you to access the property of every single item in a list IFF done so through a reference to that list before any sort of dereferencing has taken place. Once it's dereferenced, list is evaluated into individual object references, and it will no longer give you the means to enumerate through its properties in that manner.

What you get if done correctly is list containing the values of the date received property of every message of the TargetInbox, all through a single line of AppleScript.

Currently, as this wasn't how you implemented it, the declaration for set messageDate will definitely throw an error.

This explains why none of the deletions are occurring, because your script will never successfully reach that part. However, the error would occur immediately, so my feeling is the script isn't even reaching that line either, not even once. Therefore, the problem must originate from a point in the script before it ever successfully enters the inner repeat loop. And you already know my hunch about that.

Now for a potential solution

I'm sorry that I can't actually test my script to verify that this will definitely work first time without any tweaks, so tell me how you get on with this:

    using terms from application "Mail"
        on perform mail action with messages these_messages
            tell application "Mail"
                set everyMessage to a reference to messages in mailbox "News" of account "iCloud"
                repeat with eachMessage in these_messages
                    delete (everyMessage where ¬
                        the date received is not the (current date) ¬
                        and its sender is the sender of eachMessage)
                end repeat
            end tell
        end perform mail action with messages
    end using terms from

As you can see, I've done away completely with the inner most repeat loop thanks to the use of the a reference to operator. As I declared everyMessage in that manner, I am (hopefully) able to enumerate the properties of all the items the list contains in one go, which should be approximately infinity times faster than having to check the property for each item individually.

3
  • 1
    Many thanks to @CJK for not only offering a good solution, but explaining so thoroughly that I learned valuable aspects of AppleScript! See my comment on this post for my full response. Commented Jun 29, 2018 at 23:30
  • @LantzWarrick Always good to get second coders perspective even if your code works. They may have a different approach that’s better, and even if it’s not better you’ll see a different way of solving the problem. Often there’s many, many ways to solve one problem. Keep up learning AppleScript we can always use creative coders! Commented Jun 29, 2018 at 23:36
  • "should be approximately infinity times faster" 😂😂😂 👍
    – Dad
    Commented Nov 29, 2021 at 17:19
1

Many thanks to @CJK for such a thorough response. Your solution works with just one tweak, and I also learned some core concepts for AppleScript. The working script below now executes quite quickly:

using terms from application "Mail"
    on perform mail action with messages these_messages for rule this_rule
        tell application "Mail"
            set everyMessage to a reference to messages in mailbox "News" of account "iCloud"
            repeat with eachMessage in these_messages
                delete (everyMessage where ¬
                    the date received is not the (current date) ¬
                    and its reply to is the reply to of eachMessage)
            end repeat
        end tell
    end perform mail action with messages
end using terms from

The script seemed not to execute without the addition on line 2 of for rule this_rule and I surmise it is necessary in order to pass the these_messages object which is the result of the filtering defined in the parent mail.app rule which is invoking the script.

(At the end of the repeat loop, I changed sender to reply to to handle instances where the sender had changed their self-identifier string on the sending account even though the actual address remained the same, causing the script to miss older emails from the same address.)

The key understanding that I was missing is that the EveryMessage object was not automatically passed by reference. I'm coming from JavaScript, where objects, arrays, and variables are automatically passed by reference, whereas in AppleScript one must declare it as a reference object.

0

I've run into a couple of more issues implementing this script. This is the rule which executes my script.

This is the rule which executes my script. Note that the 'move' command always executes before the script, even if I try to place it after the script execution command. Mail.app didn't seem to execute the script, and I thought it might be caused by moving the incoming message into the target mailbox first, and then executing the script on the target mailbox. Since the script compares the incoming messages (eachMessage in these_messages) to messages in the target mailbox (everyMessage), perhaps there are no eachMessage objects left after the move executed to use in the script? To test my theory, I decided to incorporate the move command into my script, as follows:

using terms from application "Mail"
    on perform mail action with messages these_messages for rule this_rule
        tell application "Mail"
            set everyMessage to a reference to messages in mailbox "Charitable appeals" of account "iCloud"
            repeat with eachMessage in these_messages
                delete (everyMessage where ¬
                    the date received is not the (current date) ¬
                    and (its sender is the sender of eachMessage ¬
                    or its reply to is equal to the reply to of eachMessage))
            end repeat
            repeat with eachMessage in these_messages
                move eachMessage to mailbox "Charitable appeals" of account "iCloud"
            end repeat
        end tell
    end perform mail action with messages
end using terms from

So now my rule only executes my script on the appropriate incoming messages. The problem now is that, if I Apply Rules manually to a message from mail.app's menu, the script executes as expected, but when the rule runs automatically on incoming messages, mail.app hangs, is not responding, I have to force quit and restart, sometimes first disabling the script. I thought it might be because there were over 100 messages in the target mailbox, but then I cleaned them up to only have ~50 messages, and hanging persists.

I would greatly appreciate any help, from @CJK or anyone!

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .