# Mail Logging Script AppleScript that logs the selected Apple Mail message into your vault as an Email note and downloads its attachments. Save as `log-mail-to-obsidian.applescript` and paste it into a macOS Shortcut's Run AppleScript action (see [[08 Mail Integration]]). ```applescript -- ===================================================================== -- Log the selected Apple Mail message to a single Obsidian vault -- ===================================================================== -- Select a message in Apple Mail, run this script (e.g. from a Shortcut), -- and it creates a Markdown note for that email in your Obsidian vault. -- -- A multi-vault variant is possible: instead of one vaultRoot, you can -- inspect the message's containing account and route to different vaults. -- -- What it does: -- * Saves real attachments into the vault under -- Activities/Emails/Attachments/<YYYY>/<MonthName>/ (mirrors Daily Notes) -- * Skips inline images smaller than 10 KB (signatures, logos, tracking pixels). -- * Writes frontmatter + body sections, then opens the note in Obsidian. -- -- TO CONFIGURE: edit the three lines marked YOUR_USERNAME / YOUR_VAULT below. -- -- Your single vault root (edit this): property vaultRoot : "/Users/YOUR_USERNAME/Library/Mobile Documents/iCloud~md~obsidian/Documents/YOUR_VAULT" -- Skip attachments smaller than this many bytes (inline images, etc.) property minAttachmentBytes : 10240 -- ---- Vault identity (edit these) ---- set vaultName to "YOUR_VAULT" set isAcademic to true -- set to false if your vault has no classes/assignments -- Grab the selected message set noMessage to false set tmpAttDir to "" set savedTmpPaths to {} set savedDesiredNames to {} tell application "Mail" set sel to selection if sel is {} then set noMessage to true else set m to item 1 of sel set theSubject to (subject of m) as text set theSender to (sender of m) as text set theDate to date received of m set theMsgID to (message id of m) as text set theBody to (content of m) as text -- Save attachments to a temporary staging dir first. Skip tiny inline -- images. Each file is moved into the vault later. do shell script runs -- outside the AppleScript sandbox, so it can create dirs Shortcuts -- otherwise can't. try set tmpAttDir to "/tmp/mailatt-" & (do shell script "/usr/bin/uuidgen") do shell script "/bin/mkdir -p " & quoted form of tmpAttDir set attIndex to 0 repeat with att in (mail attachments of m) try set aName to (name of att) as text set aSize to 0 try set aSize to (file size of att) as integer end try if aSize is missing value then set aSize to 0 -- Save when size is unknown (0) OR at/above the threshold. if aSize = 0 or aSize ≥ minAttachmentBytes then set safeName to my sanitizeFile(aName) if safeName = "" then set safeName to "attachment" set attIndex to attIndex + 1 set tmpAttPath to tmpAttDir & "/" & (attIndex as text) & "_" & safeName save att in (POSIX file tmpAttPath) set end of savedTmpPaths to tmpAttPath set end of savedDesiredNames to safeName end if end try end repeat end try end if end tell if noMessage then display dialog "No message is selected in Mail. Click a message in the message list, then re-run the shortcut." buttons {"OK"} default button "OK" with icon caution return end if set emailsFolder to vaultRoot & "/Activities/Emails" -- ISO date and time set theYear to year of theDate as integer set theMonth to month of theDate as integer set theDay to day of theDate set theHour to hours of theDate set theMin to minutes of theDate set isoDate to (theYear as text) & "-" & (my pad2(theMonth)) & "-" & (my pad2(theDay)) set isoTime to (my pad2(theHour)) & ":" & (my pad2(theMin)) -- Full month name (locale-independent), matching the Daily Notes folders set monthNames to {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"} set theMonthName to item theMonth of monthNames -- Apple Mail link (URL-encoded brackets — works most reliably) set theLink to "message://%3C" & theMsgID & "%3E" -- Sanitize subject for filename set safeSubject to theSubject repeat with badChar in {":", "/", "\\", "?", "*", "\"", "<", ">", "|"} set safeSubject to my replaceText(safeSubject, contents of badChar, "") end repeat if (count of safeSubject) > 80 then set safeSubject to text 1 thru 80 of safeSubject set fileName to isoDate & " Email - " & safeSubject & ".md" set filePath to emailsFolder & "/" & fileName -- Escape quotes in subject for YAML set yamlSubject to my replaceText(theSubject, "\"", "'") set lf to linefeed -- ---- Move staged attachments into the vault, deduping names ---- -- Builds vault-relative links (attLinks) for the frontmatter + body. set attLinks to {} if (count of savedTmpPaths) > 0 then set destFolderRel to "Activities/Emails/Attachments/" & (theYear as text) & "/" & theMonthName set destFolderAbs to vaultRoot & "/" & destFolderRel repeat with i from 1 to (count of savedTmpPaths) set tp to item i of savedTmpPaths set dn to item i of savedDesiredNames try set finalName to do shell script "src=" & quoted form of tp & "; dir=" & quoted form of destFolderAbs & "; name=" & quoted form of dn & "; /bin/mkdir -p \"$dir\"; case \"$name\" in *.*) base=\"${name%.*}\"; ext=\".${name##*.}\";; *) base=\"$name\"; ext=\"\";; esac; dest=\"$dir/$base$ext\"; i=2; while [ -e \"$dest\" ]; do dest=\"$dir/$base ($i)$ext\"; i=$((i + 1)); done; /bin/mv \"$src\" \"$dest\"; /usr/bin/basename \"$dest\"" set end of attLinks to (destFolderRel & "/" & finalName) end try end repeat end if -- ---- Frontmatter (academic vaults carry classes/assignments) ---- set frontMatter to "---" & lf & ¬ "type: email" & lf & ¬ "date: " & isoDate & lf & ¬ "time: \"" & isoTime & "\"" & lf & ¬ "direction: inbound" & lf & ¬ "people: " & lf & ¬ "projects: " & lf if isAcademic then set frontMatter to frontMatter & "classes: " & lf & "assignments: " & lf end if -- attachments: list of wiki-links (empty when none) if (count of attLinks) > 0 then set frontMatter to frontMatter & "attachments:" & lf repeat with al in attLinks set frontMatter to frontMatter & " - \"[[" & (al as text) & "]]\"" & lf end repeat else set frontMatter to frontMatter & "attachments: " & lf end if set frontMatter to frontMatter & "sources: " & lf & ¬ "subject: \"" & yamlSubject & "\"" & lf & ¬ "source_link: \"" & theLink & "\"" & lf & ¬ "---" & lf & lf -- ---- "## Attachments" body section (embeds; only when present) ---- set attBody to "" if (count of attLinks) > 0 then set attBody to "## Attachments" & lf & lf repeat with al in attLinks set attBody to attBody & "![[" & (al as text) & "]]" & lf end repeat set attBody to attBody & lf end if -- ---- "+ New task from this note" button (matches the vault templates) ---- set actionsBlock to "## Actions" & lf & lf & ¬ "```dataviewjs" & lf & ¬ "const btn = dv.el(\"button\", \"+ New task from this note\");" & lf & ¬ "btn.addEventListener(\"click\", async () => {" & lf & ¬ " window._pendingTaskSource = dv.current().file.name;" & lf & ¬ " const templater = app.plugins.plugins[\"templater-obsidian\"];" & lf & ¬ " if (!templater) { new Notice(\"Templater plugin not enabled\"); return; }" & lf & ¬ " const taskTpl = app.vault.getAbstractFileByPath(\"Templates/Task.md\");" & lf & ¬ " if (!taskTpl) { new Notice(\"Templates/Task.md not found\"); return; }" & lf & ¬ " await templater.templater.create_new_note_from_template(taskTpl, \"\", \"\", true);" & lf & ¬ "});" & lf & ¬ "```" & lf & lf -- ---- Spawned-activities reverse view (matches the vault templates) ---- set spawnedBlock to "## Spawned activities" & lf & lf & ¬ "```dataview" & lf & ¬ "TABLE WITHOUT ID" & lf & ¬ " (\"[[\" + file.name + \"]]\") AS \"Note\"," & lf & ¬ " type AS \"Type\"," & lf & ¬ " date AS \"Date\"," & lf & ¬ " default(status, default(subject, default(topic, \"\"))) AS \"Detail\"" & lf & ¬ "FROM \"Activities\"" & lf & ¬ "WHERE contains(sources, this.file.link)" & lf & ¬ "SORT date DESC" & lf & ¬ "```" & lf -- ---- Assemble the note ---- set noteContent to frontMatter & ¬ "## Summary" & lf & lf & lf & ¬ "## Full content / notes" & lf & lf & ¬ "From: " & theSender & lf & lf & ¬ theBody & lf & lf & ¬ attBody & ¬ "## Source" & lf & lf & ¬ "[Open in Apple Mail](" & theLink & ")" & lf & lf & ¬ actionsBlock & ¬ spawnedBlock -- Write the file -- Workaround: Shortcuts' "Run AppleScript" sandbox blocks direct writes to iCloud Drive -- (~/Library/Mobile Documents/...) even with Full Disk Access. We write to /tmp first, -- then shell out to mv the file into the vault — `do shell script` runs outside the -- AppleScript sandbox so it can write to iCloud. try set tmpPath to "/tmp/log-mail-" & (do shell script "/usr/bin/uuidgen") & ".md" set fRef to open for access POSIX file tmpPath with write permission set eof of fRef to 0 write noteContent to fRef as «class utf8» close access fRef do shell script "/bin/mkdir -p " & quoted form of emailsFolder & ¬ " && /bin/mv " & quoted form of tmpPath & " " & quoted form of filePath on error errMsg try close access POSIX file tmpPath end try display dialog "Error writing file: " & errMsg buttons {"OK"} default button "OK" with icon stop return end try -- Clean up the temporary attachment staging dir (best effort) try if tmpAttDir is not "" then do shell script "/bin/rm -rf " & quoted form of tmpAttDir end try -- Open the new note in the vault -- Builds an `obsidian://open?vault=...&file=...` URL and hands it to `open`. -- Wrapped in its own try block so an open failure can't undo a successful save. try set vaultRelPath to "Activities/Emails/" & fileName -- URL-encode bytes, keeping RFC 3986 unreserved chars plus '/' (so path slashes survive). set encodedPath to do shell script "/usr/bin/perl -e 'for $b (unpack(\"C*\", $ARGV[0])) { if (($b >= 0x30 && $b <= 0x39) || ($b >= 0x41 && $b <= 0x5A) || ($b >= 0x61 && $b <= 0x7A) || $b == 0x2D || $b == 0x2E || $b == 0x5F || $b == 0x7E || $b == 0x2F) { print chr($b); } else { printf \"%%%02X\", $b; } }' -- " & quoted form of vaultRelPath do shell script "/usr/bin/open " & quoted form of ("obsidian://open?vault=" & vaultName & "&file=" & encodedPath) end try if (count of attLinks) > 0 then display notification (fileName & " (+" & (count of attLinks) & " attachment(s))") with title ("Logged email to " & vaultName) else display notification fileName with title ("Logged email to " & vaultName) end if -- =================== helpers =================== on pad2(n) set s to (n as integer) as text if (count of s) = 1 then return "0" & s return s end pad2 on replaceText(t, oldStr, newStr) set saveTID to AppleScript's text item delimiters set AppleScript's text item delimiters to oldStr set parts to text items of t set AppleScript's text item delimiters to newStr set joined to parts as text set AppleScript's text item delimiters to saveTID return joined end replaceText on sanitizeFile(t) -- Strip filesystem-illegal chars plus those that break Obsidian wiki-links repeat with badChar in {":", "/", "\\", "?", "*", "\"", "<", ">", "|", "[", "]", "#", "^"} set t to my replaceText(t, contents of badChar, "") end repeat return t end sanitizeFile ```