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