Revision: 5341
Initial Code
Initial URL
Initial Description
Initial Title
Initial Tags
Initial Language
at February 28, 2008 11:05 by paulhagstrom
Initial Code
(*
SmartTagMail script
Paul Hagstrom, January 2008
Usage:
Set autoTagTrigger and autoBoxTrigger to something, e.g., "autotag:" and "autobox:"
Requires (of course): MailTags.
In the notes of an address book entry, you can follow the trigger with a word.
When this rule is invoked on a message, it will look in associated address book records,
and, where it finds the trigger in the notes, it will tag/move the message accordingly.
A tag will be added for each person with a trigger associated with the message.
If one or more of the people is a member of a group whose name contains the
autotag trigger, that tag will be added as well. (e.g. "UIS autotag:uis")
Where there are multiple autobox triggers encountered, the first one found
will be acted on UNLESS a later one starts with "!", in which case it will
be then considered to be the first one found. This, the message is moved to
the first box found, or, if there are any forced boxes, then the last forced
box found.
autotag takes a tag name, autobox takes a mailbox name.
Mailbox names need to reflect the hierarchy, e.g. Lists/scriptlists
It will look in the note field from trigger: until the end of its line
I envision using this myself as a script manually called using Mail Act-On,
but if you really come to trust it, I suppose you could automatically apply it
to all incoming mail.
autoproject is not implemented yet, but should be easy.
A note about case. Email addresses in the address book need to be all
lowercase to be found. You have control over the Address Book. Fix broken ones.
You don't have control over incoming email, so those are lowercased for you.
It's a flaw in Address Book that there's no way to search for email addresses
case insensitively.
If this is called from a rule with "sender only" in the name, then only
the sender, and not the recipients, will be scanned. (Intended use is
for mail sent to big lists from a known correspondent.) In case it is useful, it
will also look for "recipients only" in the name, which will likewise
trigger scanning of the recipients and not the sender. And, finally, it will
look for "addressees only" in the name, which will scan the to:
recipients, but not the cc: recipients. The priority is sender, recipients, addressees.
If a rule name contains more than one of these keywords, only the first priority one takes effect.
*)
property autoTagTrigger : "autotag:" --text for tag trigger in notes and group names
property autoBoxTrigger : "autobox:" --text for box trigger in notes and group names
property autoProjTrigger : "autoproject:" --text for project trigger in notes and group names
property debugLevel : 2 --set from 0 to 3 depending on how detailed you want console output to be
property dryRun : false --set to false to actually move and tag, true to just pretend
property triggerNames : {"tag", "box"} --used in logging for findTrigger at debugLevel 3
(* Send debugging information to the console *)
on Logger(level, str)
if debugLevel > level then
do shell script "logger " & quoted form of ("SmartTagMail - " & str)
end if
end Logger
(* Collect emails from the current message and return them in a list *)
(* the parameters govern whether the sender's email is collected,
whether all recipients' emails are collected, and whether just the to: recipients are collected.
Note that if scanRecipients is false, the value of scanTo is irrelevant. *)
on collectEmails(msg, scanSender, scanRecipients, scanTo)
my Logger(2, "Collecting emails.")
using terms from application "Mail"
set theEmails to {}
--first check the sender, if we are supposed to
if scanSender then
set theSender to sender of msg
set theEmail to extract address from theSender
set theEmails to {theEmail}
my Logger(1, "Collect emails: Sender: " & theEmail)
end if
--now go through the recipients, if we are supposed to
if scanRecipients then
if scanTo then
set theRecipients to every to recipient of msg
else
set theRecipients to every recipient of msg
end if
repeat with theRecipient in theRecipients
set theEmail to address of theRecipient
set theEmails to theEmails & {theEmail}
my Logger(1, "Collect emails: Recipient: " & theEmail)
end repeat
end if
end using terms from
return theEmails
end collectEmails
(* Scan the note for triggers passed in as the second parameter. Multiple hits are possible, but each trigger takes the rest of its line. That is, you can have autotag:X and autotag:Y on two different lines and add both tags X and Y. The way it parses it that it cuts out everything up to the trigger and the processes the rest again. What this means is that you'll get funny results if your trigger is "autotag:" and you try to use it to tag a message with the tag "autotag:" Don't do that. *)
on findTriggers(theNote, theTriggers)
my Logger(2, "Scanning for triggers.")
set foundTriggers to {}
repeat with i from 1 to length of theTriggers
set theTrigger to item i of theTriggers
set theTail to theNote
set theResults to {}
repeat
set theOffset to offset of theTrigger in theTail
if theOffset > 0 then
set theOffset to theOffset + (length of theTrigger)
set theTail to (text theOffset thru (length of theTail) of theTail) as text
set theValue to paragraph 1 of theTail
set theResults to theResults & theValue
my Logger(2, "Trigger for " & (item i of triggerNames) & ": " & theValue)
else
exit repeat
end if
end repeat
set foundTriggers to foundTriggers & {theResults}
end repeat
return foundTriggers
end findTriggers
on processPerson(theEmail, triggerList)
tell application "Address Book"
--look for a person who has this email address (see note at top about case)
try
set foundPerson to (first person where value of every email of it contains (lowercase (theEmail)))
on error
my Logger(1, "Scan Address Book: No entry for " & theEmail)
return {}
end try
try
--scan the person's note for triggers
set theNote to (get note of foundPerson)
set theName to (get name of foundPerson)
my Logger(1, "Scan Address Book: Processing " & theName & " - " & theEmail)
set foundTriggers to (my findTriggers(theNote, triggerList))
--Look for groups that might contain additional triggers
set theGroups to every group of foundPerson
repeat with theGroup in theGroups
set statusString to ""
set theGroupName to name of theGroup
my Logger(2, "Scan Address Book: Processing group " & theGroupName)
set groupTriggers to (my findTriggers(theGroupName, triggerList))
repeat with i from 1 to length of groupTriggers
set foundItems to (a reference to item i of foundTriggers)
set contents of foundItems to contents of foundItems & item i of groupTriggers
end repeat
end repeat
return foundTriggers
on error errMsg number errNumber
my Logger(1, "Scan Address Book: Error for: " & theEmail & ": " & errMsg)
return {}
end try
end tell
end processPerson
using terms from application "Mail"
on perform mail action with messages theMessages for rule theRule
--here is where the mapping from triggers to tag/box/project happens. Most of the rest of the code is pretty general.
set triggerList to {autoTagTrigger, autoBoxTrigger}
--set triggerList to {autoTagTrigger, autoBoxTrigger, autoProjTrigger}
set tagItem to 1
set boxItem to 2
--set projectItem to 3
Logger(0, "***Starting: triggers " & triggerList)
Logger(2, "***Starting: theRule name is " & name of theRule)
set scanSender to true
set scanRecipients to true
set scanTo to false
if name of theRule contains "sender only" then
set scanRecipients to false
Logger(1, "***Scanning only sender (rule name contains sender)")
else
if name of theRule contains "recipients only" then
Logger(1, "***Scanning only recipients")
set scanSender to false
else
if name of theRule contains "addressees only" then
Logger(1, "***Scanning only to: recipients")
set scanTo to true
else
Logger(1, "***Scanning sender and all recipients")
end if
end if
end if
if dryRun then
Logger(0, "***DRY RUN")
end if
Logger(2, "***DEBUG LEVEL: " & debugLevel)
Logger(2, "Messages selected: " & (length of theMessages))
repeat with msg in theMessages
Logger(2, "Beginning message processing.")
set theEmails to my collectEmails(msg, scanSender, scanRecipients, scanTo)
set combinedTriggers to {}
repeat with theEmail in theEmails
set foundTriggers to processPerson(theEmail, triggerList)
if length of foundTriggers > 0 then --if it isn't then the person wasn't in the address book, ignore
if length of combinedTriggers is 0 then --this is the first substantive time through the loop
set combinedTriggers to foundTriggers
else
repeat with i from 1 to length of foundTriggers
set combinedItems to (a reference to item i of combinedTriggers)
set contents of combinedItems to contents of combinedItems & item i of foundTriggers
end repeat
end if
end if
end repeat
(* Having found all of the available triggers, we now deal with them. Tags first. *)
Logger(2, "Consolidating tags.")
using terms from application "MailTagsScriptingSupport"
set newTags to keywords of msg
Logger(2, "Existing tags: " & (newTags as text))
set tagsDirty to false
repeat with theTag in item tagItem of combinedTriggers
if length of theTag > 0 then
if newTags does not contain theTag then
set newTags to newTags & theTag
set tagsDirty to true
end if
end if
end repeat
end using terms from
(* Now, deal with the boxes. *)
Logger(2, "Consolidating boxes.")
set moveBox to ""
repeat with theBox in item boxItem of combinedTriggers
if character 1 of theBox is "!" then
set testBox to (text 2 thru (length of theBox) of theBox) as text
set force to true
else
set testBox to theBox
set force to false
end if
if exists mailbox testBox then
if force then
Logger(2, "Mailbox forced to: " & testBox)
set moveBox to testBox
else
if length of moveBox is 0 then
Logger(2, "Mailbox set to: " & testBox)
set moveBox to testBox
else
Logger(2, "Mailbox ignored: " & testBox)
end if
end if
else
--if the mailbox doesn't exist, ignore it
Logger(2, "Mailbox does not exist: " & testBox)
set testBox to ""
end if
end repeat
(* Later I will add project handling here too. It will work just like Boxes, there's only one. *)
(* Now, process the message. Because doing multiple things to a message can cause it to get lost, I will use the workaround proposed by ahmontgo on the indev.ca forum, and move the message first, then find it again, and perform the other operations *)
set msgID to the message id of msg
--Move if needed
if length of moveBox > 0 then
if mailbox of msg is mailbox moveBox then
Logger(0, "Already in target box: " & moveBox)
else
Logger(0, "Moving to box: " & moveBox)
if not dryRun then
set mailbox of msg to mailbox moveBox
--if we need to do more, then find the message again post-move
if tagsDirty then
set targetMessages to (messages of mailbox moveBox whose message id is msgID)
set msg to the first item of targetMessages
end if
end if
end if
end if
--Tag if needed
using terms from application "MailTagsScriptingSupport"
if tagsDirty then
Logger(0, "Setting keywords to: " & (newTags as text))
if not dryRun then
set keywords of msg to newTags
end if
end if
end using terms from
end repeat
end perform mail action with messages
end using terms from
Initial URL
Initial Description
A few weeks ago, I created an Applescript to simplify my use of Mail Act-On using MailTags. I've been using it since then and it seems to be working pretty reliably (but see note below), so I thought I'd share it in its current state. What it does is leverages information from Address Book to determine where a message should be filed, so that it is not necessary to create an individual Mail rule for each one. The main reason I did this is that I did not want to keep each sender's list of alternative email addresses both in Address Book and in a Mail rule, since any changes would then need to be made in both places. What it does is scan through the senders and recipients and looks in Address Book to see if it can find a match. If it does, it checks the Notes field for that person to see if it finds the text "autobox:" or "autotag:", and if it does, it will move the message (autobox), or tag the message (autotag), or both. I also had it check groups that found records are in, so that you can create groups of people that will be autofiled or autotagged. Since groups don't have "notes" fields, the trigger needs to be in the group name. The comments to the script elaborate the usage in more detail. I have not added automatic assignment of projects, but it is an easy extension, which when I have a chance I might add myself. It can be run in several modes, based on the name of the rule that calls it. I have set up four rules, all of them calling the script, but when the rule has "sender only" in its name, the script will check only the sender, and when the rule has "recipients only" in its name, the script will check only the recipients, etc. (see the code comments). I assigned a different MAO key to each of these rules (1, 2, 3, and 4, in fact), and use the one most appropriate to the email I'm trying to autotag and autofile. The note I wanted to add about the reliability of this is this: It generally seems to work, I haven't had any problems that I can certainly pin on this script. However, I have a couple of times had Mail flip out on me just after running this script, putting up a dialog saying that it has to quit now, and re-import the messages upon restart (that is, the envelope index got corrupted somehow). This is a pain, I have 180k messages in my local files, and reimporting (which means just rebuilding the envelope index file) takes a long time. I am kind of a novice at Applescript, so there may be various ways in which this script could be improved -- and I'd be happy to hear about them. Any speculation about what might have the consequence of corrupting the envelope index would also be very welcome. As I say, it may have nothing to do with this script, but it just happened to occur both times I've seen it recently after running the script (on a message that was already where it was supposed to be -- that is, running this a second time on the same message).
Initial Title
Auto-tag, auto-file script for Mail.app, Address Book, MailTags
Initial Tags
Initial Language
AppleScript