Posted By

paulhagstrom on 02/28/08


Tagged


Versions (?)

Auto-tag, auto-file script for Mail.app, Address Book, MailTags


 / Published in: AppleScript
 

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).

  1. (*
  2. SmartTagMail script
  3. Paul Hagstrom, January 2008
  4.  
  5. Usage:
  6. Set autoTagTrigger and autoBoxTrigger to something, e.g., "autotag:" and "autobox:"
  7.  
  8. Requires (of course): MailTags.
  9.  
  10. In the notes of an address book entry, you can follow the trigger with a word.
  11. When this rule is invoked on a message, it will look in associated address book records,
  12. and, where it finds the trigger in the notes, it will tag/move the message accordingly.
  13. A tag will be added for each person with a trigger associated with the message.
  14. If one or more of the people is a member of a group whose name contains the
  15. autotag trigger, that tag will be added as well. (e.g. "UIS autotag:uis")
  16. Where there are multiple autobox triggers encountered, the first one found
  17. will be acted on UNLESS a later one starts with "!", in which case it will
  18. be then considered to be the first one found. This, the message is moved to
  19. the first box found, or, if there are any forced boxes, then the last forced
  20. box found.
  21.  
  22. autotag takes a tag name, autobox takes a mailbox name.
  23. Mailbox names need to reflect the hierarchy, e.g. Lists/scriptlists
  24. It will look in the note field from trigger: until the end of its line
  25. I envision using this myself as a script manually called using Mail Act-On,
  26. but if you really come to trust it, I suppose you could automatically apply it
  27. to all incoming mail.
  28.  
  29. autoproject is not implemented yet, but should be easy.
  30.  
  31. A note about case. Email addresses in the address book need to be all
  32. lowercase to be found. You have control over the Address Book. Fix broken ones.
  33. You don't have control over incoming email, so those are lowercased for you.
  34. It's a flaw in Address Book that there's no way to search for email addresses
  35. case insensitively.
  36.  
  37. If this is called from a rule with "sender only" in the name, then only
  38. the sender, and not the recipients, will be scanned. (Intended use is
  39. for mail sent to big lists from a known correspondent.) In case it is useful, it
  40. will also look for "recipients only" in the name, which will likewise
  41. trigger scanning of the recipients and not the sender. And, finally, it will
  42. look for "addressees only" in the name, which will scan the to:
  43. recipients, but not the cc: recipients. The priority is sender, recipients, addressees.
  44. If a rule name contains more than one of these keywords, only the first priority one takes effect.
  45. *)
  46.  
  47. property autoTagTrigger : "autotag:" --text for tag trigger in notes and group names
  48. property autoBoxTrigger : "autobox:" --text for box trigger in notes and group names
  49. property autoProjTrigger : "autoproject:" --text for project trigger in notes and group names
  50.  
  51. property debugLevel : 2 --set from 0 to 3 depending on how detailed you want console output to be
  52. property dryRun : false --set to false to actually move and tag, true to just pretend
  53. property triggerNames : {"tag", "box"} --used in logging for findTrigger at debugLevel 3
  54.  
  55. (* Send debugging information to the console *)
  56. on Logger(level, str)
  57. if debugLevel > level then
  58. do shell script "logger " & quoted form of ("SmartTagMail - " & str)
  59. end if
  60. end Logger
  61.  
  62. (* Collect emails from the current message and return them in a list *)
  63. (* the parameters govern whether the sender's email is collected,
  64. whether all recipients' emails are collected, and whether just the to: recipients are collected.
  65. Note that if scanRecipients is false, the value of scanTo is irrelevant. *)
  66. on collectEmails(msg, scanSender, scanRecipients, scanTo)
  67. my Logger(2, "Collecting emails.")
  68. using terms from application "Mail"
  69. set theEmails to {}
  70. --first check the sender, if we are supposed to
  71. if scanSender then
  72. set theSender to sender of msg
  73. set theEmail to extract address from theSender
  74. set theEmails to {theEmail}
  75. my Logger(1, "Collect emails: Sender: " & theEmail)
  76. end if
  77. --now go through the recipients, if we are supposed to
  78. if scanRecipients then
  79. if scanTo then
  80. set theRecipients to every to recipient of msg
  81. else
  82. set theRecipients to every recipient of msg
  83. end if
  84. repeat with theRecipient in theRecipients
  85. set theEmail to address of theRecipient
  86. set theEmails to theEmails & {theEmail}
  87. my Logger(1, "Collect emails: Recipient: " & theEmail)
  88. end repeat
  89. end if
  90. end using terms from
  91. return theEmails
  92. end collectEmails
  93.  
  94. (* 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. *)
  95. on findTriggers(theNote, theTriggers)
  96. my Logger(2, "Scanning for triggers.")
  97. set foundTriggers to {}
  98. repeat with i from 1 to length of theTriggers
  99. set theTrigger to item i of theTriggers
  100. set theTail to theNote
  101. set theResults to {}
  102. repeat
  103. set theOffset to offset of theTrigger in theTail
  104. if theOffset > 0 then
  105. set theOffset to theOffset + (length of theTrigger)
  106. set theTail to (text theOffset thru (length of theTail) of theTail) as text
  107. set theValue to paragraph 1 of theTail
  108. set theResults to theResults & theValue
  109. my Logger(2, "Trigger for " & (item i of triggerNames) & ": " & theValue)
  110. else
  111. exit repeat
  112. end if
  113. end repeat
  114. set foundTriggers to foundTriggers & {theResults}
  115. end repeat
  116. return foundTriggers
  117. end findTriggers
  118.  
  119. on processPerson(theEmail, triggerList)
  120. tell application "Address Book"
  121. --look for a person who has this email address (see note at top about case)
  122. try
  123. set foundPerson to (first person where value of every email of it contains (lowercase (theEmail)))
  124. on error
  125. my Logger(1, "Scan Address Book: No entry for " & theEmail)
  126. return {}
  127. end try
  128. try
  129. --scan the person's note for triggers
  130. set theNote to (get note of foundPerson)
  131. set theName to (get name of foundPerson)
  132. my Logger(1, "Scan Address Book: Processing " & theName & " - " & theEmail)
  133. set foundTriggers to (my findTriggers(theNote, triggerList))
  134. --Look for groups that might contain additional triggers
  135. set theGroups to every group of foundPerson
  136. repeat with theGroup in theGroups
  137. set statusString to ""
  138. set theGroupName to name of theGroup
  139. my Logger(2, "Scan Address Book: Processing group " & theGroupName)
  140. set groupTriggers to (my findTriggers(theGroupName, triggerList))
  141. repeat with i from 1 to length of groupTriggers
  142. set foundItems to (a reference to item i of foundTriggers)
  143. set contents of foundItems to contents of foundItems & item i of groupTriggers
  144. end repeat
  145. end repeat
  146. return foundTriggers
  147. on error errMsg number errNumber
  148. my Logger(1, "Scan Address Book: Error for: " & theEmail & ": " & errMsg)
  149. return {}
  150. end try
  151. end tell
  152. end processPerson
  153.  
  154. using terms from application "Mail"
  155. on perform mail action with messages theMessages for rule theRule
  156. --here is where the mapping from triggers to tag/box/project happens. Most of the rest of the code is pretty general.
  157. set triggerList to {autoTagTrigger, autoBoxTrigger}
  158. --set triggerList to {autoTagTrigger, autoBoxTrigger, autoProjTrigger}
  159. set tagItem to 1
  160. set boxItem to 2
  161. --set projectItem to 3
  162. Logger(0, "***Starting: triggers " & triggerList)
  163. Logger(2, "***Starting: theRule name is " & name of theRule)
  164. set scanSender to true
  165. set scanRecipients to true
  166. set scanTo to false
  167. if name of theRule contains "sender only" then
  168. set scanRecipients to false
  169. Logger(1, "***Scanning only sender (rule name contains sender)")
  170. else
  171. if name of theRule contains "recipients only" then
  172. Logger(1, "***Scanning only recipients")
  173. set scanSender to false
  174. else
  175. if name of theRule contains "addressees only" then
  176. Logger(1, "***Scanning only to: recipients")
  177. set scanTo to true
  178. else
  179. Logger(1, "***Scanning sender and all recipients")
  180. end if
  181. end if
  182. end if
  183. if dryRun then
  184. Logger(0, "***DRY RUN")
  185. end if
  186. Logger(2, "***DEBUG LEVEL: " & debugLevel)
  187. Logger(2, "Messages selected: " & (length of theMessages))
  188. repeat with msg in theMessages
  189. Logger(2, "Beginning message processing.")
  190. set theEmails to my collectEmails(msg, scanSender, scanRecipients, scanTo)
  191. set combinedTriggers to {}
  192. repeat with theEmail in theEmails
  193. set foundTriggers to processPerson(theEmail, triggerList)
  194. if length of foundTriggers > 0 then --if it isn't then the person wasn't in the address book, ignore
  195. if length of combinedTriggers is 0 then --this is the first substantive time through the loop
  196. set combinedTriggers to foundTriggers
  197. else
  198. repeat with i from 1 to length of foundTriggers
  199. set combinedItems to (a reference to item i of combinedTriggers)
  200. set contents of combinedItems to contents of combinedItems & item i of foundTriggers
  201. end repeat
  202. end if
  203. end if
  204. end repeat
  205. (* Having found all of the available triggers, we now deal with them. Tags first. *)
  206. Logger(2, "Consolidating tags.")
  207. using terms from application "MailTagsScriptingSupport"
  208. set newTags to keywords of msg
  209. Logger(2, "Existing tags: " & (newTags as text))
  210. set tagsDirty to false
  211. repeat with theTag in item tagItem of combinedTriggers
  212. if length of theTag > 0 then
  213. if newTags does not contain theTag then
  214. set newTags to newTags & theTag
  215. set tagsDirty to true
  216. end if
  217. end if
  218. end repeat
  219. end using terms from
  220. (* Now, deal with the boxes. *)
  221. Logger(2, "Consolidating boxes.")
  222. set moveBox to ""
  223. repeat with theBox in item boxItem of combinedTriggers
  224. if character 1 of theBox is "!" then
  225. set testBox to (text 2 thru (length of theBox) of theBox) as text
  226. set force to true
  227. else
  228. set testBox to theBox
  229. set force to false
  230. end if
  231. if exists mailbox testBox then
  232. if force then
  233. Logger(2, "Mailbox forced to: " & testBox)
  234. set moveBox to testBox
  235. else
  236. if length of moveBox is 0 then
  237. Logger(2, "Mailbox set to: " & testBox)
  238. set moveBox to testBox
  239. else
  240. Logger(2, "Mailbox ignored: " & testBox)
  241. end if
  242. end if
  243. else
  244. --if the mailbox doesn't exist, ignore it
  245. Logger(2, "Mailbox does not exist: " & testBox)
  246. set testBox to ""
  247. end if
  248. end repeat
  249. (* Later I will add project handling here too. It will work just like Boxes, there's only one. *)
  250. (* 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 *)
  251. set msgID to the message id of msg
  252. --Move if needed
  253. if length of moveBox > 0 then
  254. if mailbox of msg is mailbox moveBox then
  255. Logger(0, "Already in target box: " & moveBox)
  256. else
  257. Logger(0, "Moving to box: " & moveBox)
  258. if not dryRun then
  259. set mailbox of msg to mailbox moveBox
  260. --if we need to do more, then find the message again post-move
  261. if tagsDirty then
  262. set targetMessages to (messages of mailbox moveBox whose message id is msgID)
  263. set msg to the first item of targetMessages
  264. end if
  265. end if
  266. end if
  267. end if
  268. --Tag if needed
  269. using terms from application "MailTagsScriptingSupport"
  270. if tagsDirty then
  271. Logger(0, "Setting keywords to: " & (newTags as text))
  272. if not dryRun then
  273. set keywords of msg to newTags
  274. end if
  275. end if
  276. end using terms from
  277. end repeat
  278. end perform mail action with messages
  279. end using terms from

Report this snippet  

You need to login to post a comment.