# 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
```