Files
enchiriadis d385341ce6 Helpers/scripts/_belegversand.applescript aktualisiert
Version 1.5:
Erweiterung für DATEV:
- Kein automatisiertes ZIP-Archiv der Anhänge
- Alle Belege werden einzeln in die Mail kopiert.
- Max. 20 MB an Payload, mit base64-Erweiterung Grenze bei 13 MB.
- erlaubte Datei-Typen sind: pdf, tif, tiff (alles andere wird von DATEV geblockt!)

Eine automatisierte Konvertierung in durchsuchbare PDF's ist über eine SmartRule in DEVONthink realisiert.
2026-10-12 20:24:12 +00:00

1413 lines
51 KiB
AppleScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(*
_belegversand.scpt – Version 1.5
Rolle dieses Scripts
--------------------
Dieses Script ist die eigentliche Versandlogik des Systems.
Es uebernimmt:
- Auslesen der Konfiguration aus DEVONthink
- Pruefung der aktuellen Auswahl
- Herausfiltern bereits markierter Dokumente
- Pruefung der Dateitypen (SUPPORTED_FILE_TYPES_FOR_SENDING)
- Aufteilung auf mehrere Mails nach Groesse (MAX_MAIL_SIZE_MB)
- Export in einen Arbeitsordner (Stage)
- je Mail: passwortgeschuetztes ZIP-Archiv ODER Einzeldateien (ZIP_ARCHIVE)
- Erzeugen der Mail-Entwuerfe in Apple Mail
- optionales Tagging der verarbeiteten Dokumente
- Protokollierung des Versands in NDJSON und Markdown
Version 1.5:
-----------
- ZIP_ARCHIVE=true/false: ZIP-Archiv oder Belege einzeln anhaengen
(Passwort-Dialog nur noch im ZIP-Modus)
- MAX_MAIL_SIZE_MB: Belege werden automatisch auf mehrere Mails verteilt.
Gerechnet wird mit Puffer, da Mail Anhaenge per Base64 um ca. 1/3 aufblaeht.
- SUPPORTED_FILE_TYPES_FOR_SENDING: nicht erlaubte Dateitypen werden vor
dem Versand gemeldet (Abbrechen oder ohne diese weiter)
- Stage-Ordner jetzt unter ~/Library/Caches/DT-Belegversand/
(optional per STAGE_DIR), wird nicht mehr sofort geloescht, sondern
alte Laeufe (> 7 Tage) werden beim naechsten Start aufgeraeumt
- Mehrere Mails: Betreff "(1/2)", Batch-ID "<bid>-1", Logs je Mail
- Fehlen die neuen Schluessel in der Conf, laeuft alles wie in 1.4
*)
property sentTag : "gesendet"
property defaultMailTo : "belegonline@irgendwer.de"
property defaultMailSubjectPrefix : "Belege"
property defaultLogSentGroupName : "Versendet"
property defaultWriteDebugLog : false
property defaultMailBodyFolderName : "mail_vorlagen"
property defaultMailBodyRecordName : "belegversand"
property defaultDryRunPreviewPath : "/tmp/DT-Belegversand-preview.txt"
-- Neu in 1.5
property defaultZipArchive : true
property defaultMaxMailSizeMB : 20
property defaultSupportedFileTypes : "pdf, tif, tiff"
-- Anteil der Mailgroesse, der fuer Anhaenge genutzt wird.
-- Base64 blaeht um ~37 % auf (inkl. Zeilenumbrueche), dazu Header/Text.
property usableMailShare : 0.7
-- Alte Stage-Laeufe werden nach so vielen Tagen geloescht.
property stageKeepDays : 7
property appVersion : "1.5"
property basePath : "Library/Application Scripts/com.devon-technologies.think/Menu/Belegversand"
property configGroupName : "_config"
property configRecordName : "belegversand.conf"
property debugLogPath : "/tmp/DT-Belegversand-debug.log"
property windowTitle : "Belegversand, Vers. " & appVersion
property cfgConfigRecordUUID : missing value
property cfgMailToDefault : missing value
property cfgMailCcToDefault : missing value
property cfgMailSubjectPrefix : missing value
property cfgZipPasswordDefault : missing value
property cfgLogSentGroupName : missing value
property cfgBelegeRootUUID : missing value
property cfgWriteDebugLog : missing value
property cfgMailBodyFolderName : missing value
property cfgMailBodyRecordName : missing value
property cfgSentTagForDocs : missing value
property cfgDryRunPreviewPath : missing value
property cfgZipArchive : missing value
property cfgMaxMailSizeMB : missing value
property cfgSupportedTypes : missing value
property cfgSupportedTypesText : missing value
property cfgStageDir : missing value
-- =============================================================================
-- Debug
-- =============================================================================
on writeDebug(msg)
if my writeDebugLogEnabled() is false then return
set ts to do shell script "date '+%Y-%m-%d %H:%M:%S'"
do shell script "printf %s\\\\n " & quoted form of ("[" & ts & "] " & msg) & " >> " & quoted form of my debugLogPath
end writeDebug
on resetDebugLog()
if my writeDebugLogEnabled() is false then return
do shell script "rm -f " & quoted form of my debugLogPath
my writeDebug("=== Debug-Session gestartet (v" & appVersion & ") ===")
end resetDebugLog
on writeDebugLogEnabled()
try
if my cfgWriteDebugLog is missing value then return defaultWriteDebugLog
return my cfgWriteDebugLog
on error
return defaultWriteDebugLog
end try
end writeDebugLogEnabled
-- =============================================================================
-- Allgemeine Helfer
-- =============================================================================
on isoTimestamp()
return (do shell script "date '+%Y-%m-%dT%H:%M:%S%z' | sed -E 's/([+-][0-9]{2})([0-9]{2})/\\1:\\2/'")
end isoTimestamp
on batchId()
return (do shell script "date '+%Y%m%d-%H%M%S'")
end batchId
on ensureFolder(posixPath)
do shell script "mkdir -p " & quoted form of posixPath
end ensureFolder
on jsonEscape(s)
if s is missing value then set s to ""
set s to s as text
set s to my replaceText("\\", "\\\\", s)
set s to my replaceText("\"", "\\\"", s)
set s to my replaceText(return, "\\n", s)
return s
end jsonEscape
on mdEscape(s)
if s is missing value then set s to ""
set s to s as text
set s to my replaceText("|", "\\|", s)
set s to my replaceText(return, " ", s)
return s
end mdEscape
on replaceText(find, repl, txt)
set oldTIDs to AppleScript's text item delimiters
try
if txt is missing value then set txt to ""
set txt to txt as text
set AppleScript's text item delimiters to find
set parts to every text item of txt
set AppleScript's text item delimiters to repl
set txt2 to parts as text
set AppleScript's text item delimiters to oldTIDs
return txt2
on error errMsg number errNum
set AppleScript's text item delimiters to oldTIDs
error errMsg number errNum
end try
end replaceText
on trimText(s)
set s to s as text
set s to do shell script "printf %s " & quoted form of s & " | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'"
return s
end trimText
-- Kommagetrennte Liste in AppleScript-Liste (getrimmt, ohne Leereintraege)
on parseCSVList(csvText)
if csvText is "" or csvText is missing value then return {}
set csvText to csvText as text
set outList to {}
set saveDelimiters to AppleScript's text item delimiters
try
set AppleScript's text item delimiters to ","
set parts to every text item of csvText
set AppleScript's text item delimiters to saveDelimiters
repeat with part in parts
set trimmedPart to my trimText(part as text)
if trimmedPart is not "" then
set end of outList to trimmedPart
end if
end repeat
return outList
on error errMsg
set AppleScript's text item delimiters to saveDelimiters
my writeDebug("parseCSVList error: " & errMsg)
return {}
end try
end parseCSVList
on parseCSVEmails(csvText)
return my parseCSVList(csvText)
end parseCSVEmails
-- Bytes als "12.3" (MB, eine Nachkommastelle)
on formatMB(bytes)
return do shell script "awk 'BEGIN{printf \"%.1f\", " & (bytes as integer) & "/1048576}'"
end formatMB
-- Dateiendung (ohne Punkt), "" wenn keine vorhanden
on fileExtension(fileName)
set fileName to fileName as text
if fileName does not contain "." then return ""
set oldTIDs to AppleScript's text item delimiters
set AppleScript's text item delimiters to "."
set ext to last text item of fileName
set AppleScript's text item delimiters to oldTIDs
return ext
end fileExtension
-- =============================================================================
-- DEVONthink-Records
-- =============================================================================
on isGroupByUUID(recordUUID)
tell application id "DNtp"
set r to get record with uuid recordUUID
try
return ((type of r) is group)
on error errMsg
my writeDebug("isGroupByUUID error uuid=" & recordUUID & " msg=" & errMsg)
return false
end try
end tell
end isGroupByUUID
on getDTRecordTextByUUID(recordUUID)
tell application id "DNtp"
set theRecord to get record with uuid recordUUID
try
set oldText to plain text of theRecord
if oldText is missing value then set oldText to ""
return oldText as rich text
on error
try
set oldText to content of theRecord
if oldText is missing value then set oldText to ""
return oldText as rich text
on error
return ""
end try
end try
end tell
end getDTRecordTextByUUID
on setDTRecordTextByUUID(recordUUID, newText)
tell application id "DNtp"
set theRecord to get record with uuid recordUUID
try
set content of theRecord to newText
on error
try
set plain text of theRecord to newText
on error errMsg
error "setDTRecordTextByUUID failed: " & errMsg
end try
end try
end tell
end setDTRecordTextByUUID
on appendLineToDTRecordByUUID(recordUUID, lineText)
my writeDebug("appendLineToDTRecordByUUID START recordUUID=" & recordUUID)
set oldText to my getDTRecordTextByUUID(recordUUID)
if oldText is "" then
set newText to lineText & return
else if oldText ends with return then
set newText to oldText & lineText & return
else
set newText to oldText & return & lineText & return
end if
my setDTRecordTextByUUID(recordUUID, newText)
my writeDebug("appendLineToDTRecordByUUID DONE recordUUID=" & recordUUID)
end appendLineToDTRecordByUUID
on getSelectedUUIDs()
tell application id "DNtp"
set sel to selection
if sel is {} then error "Keine Objekte ausgewählt."
set selCount to count of sel
set rawList to {}
repeat with i from 1 to selCount
set anItem to item i of sel
set end of rawList to (uuid of anItem as rich text)
end repeat
end tell
set outList to {}
repeat with itemUUID in rawList
set uuidText to itemUUID as text
if my isGroupByUUID(uuidText) then
my writeDebug("Selection ignored group uuid=" & uuidText)
else
set end of outList to uuidText
end if
end repeat
return {selCount, outList}
end getSelectedUUIDs
on recordHasTagByUUID(recordUUID, tagName)
tell application id "DNtp"
set r to get record with uuid recordUUID
try
set t to tags of r
if t is missing value then return false
return (t contains tagName)
on error
return false
end try
end tell
end recordHasTagByUUID
on filterUUIDsWithoutTag(uuidList, tagName)
set outList to {}
repeat with thisUUID in uuidList
set uuidText to thisUUID as text
set hasTag to my recordHasTagByUUID(uuidText, tagName)
my writeDebug("Tag-Check uuid=" & uuidText & " hasTag=" & (hasTag as text))
if hasTag is false then
set end of outList to uuidText
end if
end repeat
return outList
end filterUUIDsWithoutTag
on findChildRecordUUIDByName(parentUUID, childName)
tell application id "DNtp"
set parentRecord to get record with uuid parentUUID
try
set kids to children of parentRecord
repeat with i from 1 to count of kids
set c to item i of kids
if (name of c as rich text) is childName then return (uuid of c as rich text)
end repeat
end try
end tell
return missing value
end findChildRecordUUIDByName
on getOrCreateChildGroupUUID(parentUUID, groupName)
set existingUUID to my findChildRecordUUIDByName(parentUUID, groupName)
if existingUUID is not missing value then
my writeDebug("Group exists: " & groupName & " uuid=" & existingUUID)
return existingUUID
end if
tell application id "DNtp"
set parentRecord to get record with uuid parentUUID
set newGroup to create record with {name:groupName, type:group} in parentRecord
set newUUID to uuid of newGroup as rich text
my writeDebug("Group created: " & groupName & " uuid=" & newUUID)
return newUUID
end tell
end getOrCreateChildGroupUUID
on getOrCreateTextLogUUID(parentUUID, childName, initialText)
set existingUUID to my findChildRecordUUIDByName(parentUUID, childName)
if existingUUID is not missing value then
my writeDebug("Text log exists: " & childName & " uuid=" & existingUUID)
return existingUUID
end if
tell application id "DNtp"
set parentRecord to get record with uuid parentUUID
set newRecord to create record with {name:childName, type:txt, content:initialText} in parentRecord
set newUUID to uuid of newRecord as rich text
my writeDebug("Text log created: " & childName & " uuid=" & newUUID)
return newUUID
end tell
end getOrCreateTextLogUUID
on getOrCreateMarkdownLogUUID(parentUUID, childName, initialText)
set existingUUID to my findChildRecordUUIDByName(parentUUID, childName)
if existingUUID is not missing value then
my writeDebug("Markdown log exists: " & childName & " uuid=" & existingUUID)
return existingUUID
end if
tell application id "DNtp"
set parentRecord to get record with uuid parentUUID
set newRecord to create record with {name:childName, type:markdown, content:initialText} in parentRecord
set newUUID to uuid of newRecord as rich text
my writeDebug("Markdown log created: " & childName & " uuid=" & newUUID)
return newUUID
end tell
end getOrCreateMarkdownLogUUID
on getRecordNameByUUID(recordUUID)
tell application id "DNtp"
set r to get record with uuid recordUUID
try
return name of r as rich text
on error
return ""
end try
end tell
end getRecordNameByUUID
on getRecordFileNameByUUID(recordUUID)
tell application id "DNtp"
set r to get record with uuid recordUUID
try
set fn to filename of r
if fn is not missing value then
set fnText to fn as rich text
if fnText is not "" then return fnText
end if
end try
try
return name of r as rich text
on error
return ""
end try
end tell
end getRecordFileNameByUUID
-- Dateiendung eines Records: zuerst aus "filename", sonst aus "path".
-- (Der DEVONthink-Typ allein reicht nicht: JPG und TIFF sind beide "picture".)
on getRecordExtensionByUUID(recordUUID)
set fnText to ""
tell application id "DNtp"
set r to get record with uuid recordUUID
try
set fn to filename of r
if fn is not missing value then set fnText to fn as rich text
end try
if fnText is "" then
try
set p to path of r
if p is not missing value then set fnText to p as rich text
end try
end if
end tell
return my fileExtension(fnText)
end getRecordExtensionByUUID
on getRecordSizeByUUID(recordUUID)
tell application id "DNtp"
set r to get record with uuid recordUUID
try
return (size of r) as integer
on error
return 0
end try
end tell
end getRecordSizeByUUID
on getReferenceURLByUUID(recordUUID)
tell application id "DNtp"
set r to get record with uuid recordUUID
try
return reference URL of r as rich text
on error
return ""
end try
end tell
end getReferenceURLByUUID
on addTagByUUID(recordUUID, tagName)
my writeDebug("addTag START uuid=" & recordUUID)
tell application id "DNtp"
set r to get record with uuid recordUUID
set t to {}
try
set t to tags of r
if t is missing value then set t to {}
on error errMsg
my writeDebug("addTag tags-read error uuid=" & recordUUID & " msg=" & errMsg)
set t to {}
end try
if t does not contain tagName then
set end of t to tagName
set tags of r to t
my writeDebug("addTag wrote tag uuid=" & recordUUID)
else
my writeDebug("addTag tag already present uuid=" & recordUUID)
end if
end tell
set verifyTag to my recordHasTagByUUID(recordUUID, tagName)
my writeDebug("addTag DONE uuid=" & recordUUID & " verify=" & (verifyTag as text))
end addTagByUUID
on exportRecordByUUID(recordUUID, targetFolderAlias)
my writeDebug("export START uuid=" & recordUUID)
tell application id "DNtp"
set r to get record with uuid recordUUID
if r is missing value then error "Record nicht gefunden für UUID: " & recordUUID
export record r to targetFolderAlias
end tell
my writeDebug("export DONE uuid=" & recordUUID)
end exportRecordByUUID
on findChildRecordByName(parentRecord, childName)
tell application id "DNtp"
try
set kids to children of parentRecord
repeat with i from 1 to count of kids
set c to item i of kids
if (name of c as rich text) is childName then return c
end repeat
end try
end tell
return missing value
end findChildRecordByName
on getTopParentGroupFromUUID(recordUUID)
tell application id "DNtp"
set currentRecord to get record with uuid recordUUID
repeat
try
set p to parents of currentRecord
if p is {} then return currentRecord
set currentRecord to item 1 of p
on error
return currentRecord
end try
end repeat
end tell
end getTopParentGroupFromUUID
-- =============================================================================
-- Konfiguration
-- =============================================================================
on findConfigRecordUUIDFromCurrentSelection()
tell application id "DNtp"
set sel to selection
if sel is {} then error "Keine Objekte ausgewählt."
set selectedRecord to item 1 of sel
set selectedUUID to uuid of selectedRecord as rich text
end tell
set dbRootRecord to my getTopParentGroupFromUUID(selectedUUID)
set configGroupRecord to my findChildRecordByName(dbRootRecord, my configGroupName)
if configGroupRecord is missing value then error "Keine Konfiguration gefunden (_config fehlt). Bitte zuerst das Setup-Script ausführen."
set configRecord to my findChildRecordByName(configGroupRecord, my configRecordName)
if configRecord is missing value then error "Keine Konfiguration gefunden (" & my configRecordName & " fehlt). Bitte zuerst das Setup-Script ausführen."
tell application id "DNtp"
return uuid of configRecord as rich text
end tell
end findConfigRecordUUIDFromCurrentSelection
on configValue(configText, keyName)
set keyPrefix to keyName & "="
repeat with oneLine in paragraphs of configText
set lineText to oneLine as text
if lineText starts with keyPrefix then
if (length of lineText) is equal to (length of keyPrefix) then
return ""
else
set val to text ((length of keyPrefix) + 1) thru -1 of lineText
return my trimText(val)
end if
end if
end repeat
return missing value
end configValue
-- true/false-Wert aus der Conf, mit Default bei fehlendem/leerem Schluessel
on configBool(configText, keyName, defaultValue)
set v to my configValue(configText, keyName)
if v is missing value or v is "" then return defaultValue
set vLower to do shell script "printf %s " & quoted form of v & " | tr '[:upper:]' '[:lower:]'"
return (vLower is "true")
end configBool
on loadConfig()
set configUUID to my findConfigRecordUUIDFromCurrentSelection()
set cfgText to my getDTRecordTextByUUID(configUUID)
if cfgText is "" then error "Konfigurationsdatei ist leer: " & my configRecordName
set vMailTo to my configValue(cfgText, "MAIL_TO_DEFAULT")
if vMailTo is missing value then set vMailTo to defaultMailTo
set vMailCcTo to my configValue(cfgText, "MAIL_CC_TO_DEFAULT")
if vMailCcTo is missing value or vMailCcTo is "" then set vMailCcTo to ""
set vSubjectPrefix to my configValue(cfgText, "MAIL_SUBJECT_PREFIX")
if vSubjectPrefix is missing value then set vSubjectPrefix to defaultMailSubjectPrefix
set vZipPasswordDefault to my configValue(cfgText, "ZIP_PASSWORD_DEFAULT")
if vZipPasswordDefault is missing value then set vZipPasswordDefault to ""
set vLogSentGroupName to my configValue(cfgText, "LOG_SENT_GROUP_NAME")
if vLogSentGroupName is missing value then set vLogSentGroupName to defaultLogSentGroupName
set vBelegeRootUUID to my configValue(cfgText, "BELEGE_ROOT_UUID")
if vBelegeRootUUID is missing value or vBelegeRootUUID is "" then error "BELEGE_ROOT_UUID fehlt in " & my configRecordName
set vDebugBool to my configBool(cfgText, "WRITE_DEBUG_LOG", defaultWriteDebugLog)
set vMailBodyFolderName to my configValue(cfgText, "MAILBODY_FOLDER_NAME")
if vMailBodyFolderName is missing value or vMailBodyFolderName is "" then set vMailBodyFolderName to defaultMailBodyFolderName
set vMailBodyRecordName to my configValue(cfgText, "MAILBODY_RECORD_NAME")
if vMailBodyRecordName is missing value or vMailBodyRecordName is "" then set vMailBodyRecordName to defaultMailBodyRecordName
set vSentTagForDocs to my configValue(cfgText, "SENT_TAG_FOR_DOCS")
if vSentTagForDocs is missing value or vSentTagForDocs is "" then set vSentTagForDocs to sentTag
set vDryRunPreviewPath to my configValue(cfgText, "PATH_TO_DRYRUN_PREVIEW")
if vDryRunPreviewPath is missing value or vDryRunPreviewPath is "" then set vDryRunPreviewPath to defaultDryRunPreviewPath
-- Neu in 1.5 ------------------------------------------------------------
set vZipArchive to my configBool(cfgText, "ZIP_ARCHIVE", defaultZipArchive)
set vMaxMB to defaultMaxMailSizeMB
set vMaxMBText to my configValue(cfgText, "MAX_MAIL_SIZE_MB")
if vMaxMBText is not missing value and vMaxMBText is not "" then
try
set vMaxMB to vMaxMBText as number
if vMaxMB is less than or equal to 0 then set vMaxMB to defaultMaxMailSizeMB
on error
set vMaxMB to defaultMaxMailSizeMB
end try
end if
set vTypesText to my configValue(cfgText, "SUPPORTED_FILE_TYPES_FOR_SENDING")
if vTypesText is missing value or vTypesText is "" then set vTypesText to defaultSupportedFileTypes
set vTypesRaw to my parseCSVList(vTypesText)
set vTypes to {}
repeat with t in vTypesRaw
set tText to t as text
if tText starts with "." then set tText to text 2 thru -1 of tText
if tText is not "" then set end of vTypes to tText
end repeat
set vStageDir to my configValue(cfgText, "STAGE_DIR")
if vStageDir is missing value then set vStageDir to ""
return {configUUID:configUUID, mailToDefault:vMailTo, mailCcToDefault:vMailCcTo, subjectPrefix:vSubjectPrefix, zipPasswordDefault:vZipPasswordDefault, logSentGroupName:vLogSentGroupName, belegeRootUUID:vBelegeRootUUID, writeDebugLog:vDebugBool, mailBodyFolderName:vMailBodyFolderName, mailBodyRecordName:vMailBodyRecordName, sentTagForDocs:vSentTagForDocs, dryRunPreviewPath:vDryRunPreviewPath, zipArchive:vZipArchive, maxMailSizeMB:vMaxMB, supportedTypes:vTypes, supportedTypesText:vTypesText, stageDir:vStageDir}
end loadConfig
-- =============================================================================
-- Mail-Vorlagen
-- =============================================================================
on runTemplateSelector()
set expandedPath to (do shell script "echo ~") & "/" & basePath
set scriptPath to expandedPath & "/Helpers/show_mail_templates.scpt"
try
set loadedScript to load script (POSIX file scriptPath as alias)
set standaloneMode of loadedScript to false
set templateResult to selectTemplate() of loadedScript
return templateResult
on error errMsg
display dialog "FEHLER: " & errMsg buttons {"OK"}
return ""
end try
end runTemplateSelector
on getMailBodyTextFromCurrentDB()
tell application id "DNtp"
set sel to selection
if sel is {} then return ""
set selectedRecord to item 1 of sel
set selectedUUID to uuid of selectedRecord as rich text
end tell
set dbRootRecord to my getTopParentGroupFromUUID(selectedUUID)
set configGroupRecord to my findChildRecordByName(dbRootRecord, my configGroupName)
if configGroupRecord is missing value then return ""
set mailBodyFolderRecord to my findChildRecordByName(configGroupRecord, my cfgMailBodyFolderName)
if mailBodyFolderRecord is missing value then return ""
set templateName to my runTemplateSelector()
my writeDebug("runTemplateSelector returned: '" & templateName & "'")
if templateName is "" then
my writeDebug("templateName ist leer, fallback zu default: " & my cfgMailBodyRecordName)
set templateName to my cfgMailBodyRecordName
end if
my writeDebug("Suche nach Mailbody-Record: '" & templateName & "'")
set mailBodyRecord to my findChildRecordByName(mailBodyFolderRecord, templateName)
if mailBodyRecord is missing value then
my writeDebug("ERROR: mailBodyRecord nicht gefunden!")
return ""
end if
tell application id "DNtp"
try
set t to plain text of mailBodyRecord
if t is missing value then set t to ""
return t as rich text
on error
try
set t to content of mailBodyRecord
if t is missing value then set t to ""
return t as rich text
on error
return ""
end try
end try
end tell
end getMailBodyTextFromCurrentDB
-- =============================================================================
-- Neu in 1.5: Dateitypen, Groessen, Pakete, Stage
-- =============================================================================
on isSupportedExtension(ext)
if ext is "" then return false
-- AppleScript vergleicht Text standardmaessig ohne Gross-/Kleinschreibung
repeat with t in my cfgSupportedTypes
if ext is (t as text) then return true
end repeat
return false
end isSupportedExtension
-- Teilt eine UUID-Liste in {erlaubt, nicht erlaubt}
on splitBySupportedType(uuidList)
set okList to {}
set badList to {}
repeat with u in uuidList
set uText to u as text
set ext to my getRecordExtensionByUUID(uText)
if my isSupportedExtension(ext) then
set end of okList to uText
else
set end of badList to uText
my writeDebug("Typ nicht erlaubt uuid=" & uText & " ext='" & ext & "'")
end if
end repeat
return {okList, badList}
end splitBySupportedType
-- Namensliste fuer Dialoge, gekuerzt auf maxShown Eintraege
on namesListText(uuidList, maxShown, withSize)
set outText to ""
set shownCount to 0
repeat with u in uuidList
if shownCount is greater than or equal to maxShown then exit repeat
set uText to u as text
set lineText to "• " & my getRecordFileNameByUUID(uText)
if withSize then set lineText to lineText & " (" & my formatMB(my getRecordSizeByUUID(uText)) & " MB)"
set outText to outText & lineText & return
set shownCount to shownCount + 1
end repeat
if (count of uuidList) is greater than maxShown then
set outText to outText & "… und " & ((count of uuidList) - maxShown) & " weitere" & return
end if
return outText
end namesListText
-- Netto-Budget fuer Anhaenge pro Mail in Bytes
on attachmentBudgetBytes()
return ((my cfgMaxMailSizeMB) * 1048576 * usableMailShare) as integer
end attachmentBudgetBytes
(*
buildPackages(uuidList, limitBytes)
-----------------------------------
Verteilt Belege auf moeglichst wenige Mails ("First Fit Decreasing"):
groesste Belege zuerst, jeder Beleg in die erste Mail, in die er noch passt.
Belege, die allein schon groesser als das Budget sind, landen in "oversized".
Rueckgabe: {packages:{{uuid,...},...}, sizes:{bytes,...}, oversized:{uuid,...}}
*)
on buildPackages(uuidList, limitBytes)
set itemsList to {}
repeat with u in uuidList
set uText to u as text
set end of itemsList to {uuidText:uText, sz:(my getRecordSizeByUUID(uText))}
end repeat
-- absteigend nach Groesse sortieren
set n to count of itemsList
repeat with i from 1 to n - 1
set maxIdx to i
repeat with j from i + 1 to n
if (sz of item j of itemsList) > (sz of item maxIdx of itemsList) then set maxIdx to j
end repeat
if maxIdx is not i then
set tmp to item i of itemsList
set item i of itemsList to item maxIdx of itemsList
set item maxIdx of itemsList to tmp
end if
end repeat
set pkgList to {}
set pkgSizes to {}
set oversizedList to {}
repeat with anEntry in itemsList
set s to sz of anEntry
set u to uuidText of anEntry
if s > limitBytes then
set end of oversizedList to u
else
set placed to false
repeat with k from 1 to count of pkgSizes
if ((item k of pkgSizes) + s) is less than or equal to limitBytes then
set p to item k of pkgList
set end of p to u
set item k of pkgSizes to (item k of pkgSizes) + s
set placed to true
exit repeat
end if
end repeat
if not placed then
set end of pkgList to {u}
set end of pkgSizes to s
end if
end if
end repeat
return {packages:pkgList, sizes:pkgSizes, oversized:oversizedList}
end buildPackages
on stageBaseDir()
set homeDir to POSIX path of (path to home folder)
set p to my cfgStageDir
if p is missing value or p is "" then return homeDir & "Library/Caches/DT-Belegversand/"
set p to p as text
if p starts with "~/" then set p to homeDir & (text 3 thru -1 of p)
if p does not end with "/" then set p to p & "/"
return p
end stageBaseDir
-- Loescht alte Stage-Laeufe. Aus Sicherheitsgruenden nur, wenn der Pfad
-- erkennbar ein Belegversand-Ordner ist (nie versehentlich ~ oder /).
on cleanupOldStages(baseDir)
if baseDir does not contain "Belegversand" then
my writeDebug("cleanupOldStages übersprungen (Pfad enthält nicht 'Belegversand'): " & baseDir)
return
end if
try
do shell script "test -d " & quoted form of baseDir & " && find " & quoted form of baseDir & " -mindepth 1 -maxdepth 1 -type d -mtime +" & stageKeepDays & " -exec rm -rf {} + 2>/dev/null || true"
my writeDebug("cleanupOldStages done: " & baseDir)
on error errMsg
my writeDebug("cleanupOldStages error: " & errMsg)
end try
end cleanupOldStages
-- =============================================================================
-- Dry Run
-- =============================================================================
on safeDryRunPreviewPath()
set p to my cfgDryRunPreviewPath
if p is missing value or p is "" then set p to defaultDryRunPreviewPath
return p as text
end safeDryRunPreviewPath
on modeText()
if my cfgZipArchive then return "ZIP-Archiv"
return "Einzeldateien"
end modeText
on writeDryRunPreviewFile(pkgList, pkgSizes, theTo)
set previewPath to my safeDryRunPreviewPath()
set previewDir to do shell script "dirname " & quoted form of previewPath
do shell script "mkdir -p " & quoted form of previewDir
set nMails to count of pkgList
set nItems to 0
repeat with p in pkgList
set nItems to nItems + (count of p)
end repeat
set previewText to "Belegversand – Vorschau (Dry Run)" & return & return & ¬
"Empfänger: " & theTo & return & ¬
"Versandmodus: " & my modeText() & return & ¬
"Anzahl Belege: " & nItems & return & ¬
"Anzahl Mails: " & nMails & " (max. " & my cfgMaxMailSizeMB & " MB je Mail)" & return
set i to 1
repeat with k from 1 to nMails
set previewText to previewText & return & "Mail " & k & " von " & nMails & " – ca. " & my formatMB(item k of pkgSizes) & " MB Anhänge:" & return
repeat with u in (item k of pkgList)
set uText to u as text
set previewText to previewText & i & ". " & my getRecordFileNameByUUID(uText) & " (" & my formatMB(my getRecordSizeByUUID(uText)) & " MB)" & return
set i to i + 1
end repeat
end repeat
do shell script "cat > " & quoted form of previewPath & " <<'EOF'" & linefeed & previewText & "EOF"
return previewPath
end writeDryRunPreviewFile
on dryRunSummaryText(toSendUUIDs, nMails, previewPath)
set nItems to count of toSendUUIDs
set outText to "Insgesamt würden " & nItems & " Belege verarbeitet," & return & ¬
"verteilt auf " & nMails & " Mail(s) – Modus: " & my modeText() & "." & return & return
set outText to outText & my namesListText(toSendUUIDs, 4, false)
set outText to outText & return & "Die gesamte Liste findest du hier:" & return & previewPath
return outText
end dryRunSummaryText
on showDryRunPreviewInFinder(previewPath)
do shell script "open -R " & quoted form of previewPath
end showDryRunPreviewInFinder
-- =============================================================================
-- Buchungsjahre
-- =============================================================================
on isYearGroupName(groupName)
try
if (length of groupName) is not 4 then return false
do shell script "printf %s " & quoted form of groupName & " | /usr/bin/grep -Eq '^[0-9]{4}$'"
return true
on error
return false
end try
end isYearGroupName
on getBookingYearInfoFromUUID(recordUUID)
tell application id "DNtp"
set currentRecord to get record with uuid recordUUID
repeat
try
set p to parents of currentRecord
if p is {} then return missing value
set parentRecord to item 1 of p
set parentName to name of parentRecord as rich text
if my isYearGroupName(parentName) then
return {yearUUID:(uuid of parentRecord as rich text), yearName:parentName}
end if
set currentRecord to parentRecord
on error errMsg
my writeDebug("getBookingYearInfoFromUUID error: " & errMsg)
return missing value
end try
end repeat
end tell
end getBookingYearInfoFromUUID
on getSelectedBookingYearInfo(uuidList)
set firstYearUUID to missing value
set firstYearName to missing value
set hasOutsiders to false
repeat with thisUUID in uuidList
set uuidText to thisUUID as text
set yearInfo to my getBookingYearInfoFromUUID(uuidText)
if yearInfo is missing value then
set hasOutsiders to true
my writeDebug("BookingYearCheck uuid=" & uuidText & " -> Außenseiter (kein Jahr gefunden)")
else
set thisYearUUID to (yearUUID of yearInfo) as text
set thisYearName to (yearName of yearInfo) as text
my writeDebug("BookingYearCheck uuid=" & uuidText & " yearName=" & thisYearName & " yearUUID=" & thisYearUUID)
if firstYearUUID is missing value then
set firstYearUUID to thisYearUUID
set firstYearName to thisYearName
else
if thisYearUUID is not equal to firstYearUUID then
error "Es wurden Belege aus mehreren Buchungsjahren ausgewählt." & return & return & "Bitte pro Sendelauf nur Belege aus einem Jahr auswählen."
end if
end if
end if
end repeat
if hasOutsiders and firstYearUUID is not missing value then
error "Es wurden Belege aus Buchungsjahren UND von außerhalb ausgewählt." & return & return & "Bitte nur Belege aus einem Bereich wählen."
end if
return {yearUUID:firstYearUUID, yearName:firstYearName, hasOutsiders:hasOutsiders}
end getSelectedBookingYearInfo
on getOrCreateGlobalSentGroupUUID(referenceUUID)
set dbRootRecord to my getTopParentGroupFromUUID(referenceUUID)
tell application id "DNtp"
set dbRootUUID to uuid of dbRootRecord as rich text
end tell
my writeDebug("getOrCreateGlobalSentGroupUUID dbRootUUID=" & dbRootUUID)
set globalSentGroupUUID to my getOrCreateChildGroupUUID(dbRootUUID, "Versendet_Outsider")
return globalSentGroupUUID
end getOrCreateGlobalSentGroupUUID
-- =============================================================================
-- Ergebnis & Meldungen
-- =============================================================================
on joinList(theList, sep)
set outText to ""
repeat with anItem in theList
if outText is "" then
set outText to (anItem as text)
else
set outText to outText & sep & (anItem as text)
end if
end repeat
return outText
end joinList
on writeResultRecordByUUID(recordUUID, ts, bid, toAddrs, ccAddrs, nItems, nMails, isOutsider)
my writeDebug("writeResultRecordByUUID START recordUUID=" & recordUUID)
set toDisplay to my joinList(toAddrs, ", ")
set ccDisplay to my joinList(ccAddrs, ", ")
if ccDisplay is "" then set ccDisplay to "(keine)"
if nMails is 1 then
set draftText to "Der Mail-Entwurf wurde in Apple Mail erstellt."
else
set draftText to nMails & " Mail-Entwürfe wurden in Apple Mail erstellt."
end if
set resultText to "# Letzter Sendelauf – Ergebnis" & return & return & ¬
"**Zeitstempel:** " & ts & return & ¬
"**Batch-ID:** " & bid & return & ¬
"**Empfänger (TO):** " & toDisplay & return & ¬
"**Empfänger (CC):** " & ccDisplay & return & ¬
"**Versandmodus:** " & my modeText() & return & ¬
"**Anzahl Belege:** " & nItems & return & ¬
"**Anzahl Mails:** " & nMails & return & ¬
"**Status:** ok Erfolgreich" & return & return & ¬
draftText
if isOutsider then
set resultText to resultText & return & return & "*Hinweis: Dies waren Außenseiter-Belege (von außerhalb eines Buchungsjahres).*"
end if
try
my setDTRecordTextByUUID(recordUUID, resultText)
my writeDebug("writeResultRecordByUUID text set OK")
on error errMsg
my writeDebug("writeResultRecordByUUID ERROR: " & errMsg)
end try
end writeResultRecordByUUID
on showCompletionNotification(nItems, nMails)
try
if nMails is 1 then
set msgText to (nItems as text) & " Beleg(e) – Mail-Entwurf erstellt."
else
set msgText to (nItems as text) & " Beleg(e) – " & nMails & " Mail-Entwürfe erstellt."
end if
tell application id "DNtp"
display notification msgText with title "Belegversand" subtitle "Bitte Entwürfe prüfen und senden."
end tell
on error errMsg
my writeDebug("showCompletionNotification ERROR: " & errMsg)
end try
end showCompletionNotification
on friendlyRunHeader(ts)
try
return do shell script "LANG=de_DE.UTF-8 date -j -f '%Y-%m-%dT%H:%M:%S%z' " & quoted form of ((do shell script "printf %s " & quoted form of ts & " | sed 's/://3'")) & " '+Sendelauf am %-d. %B %Y, Uhrzeit %H:%M:%S'"
on error
return "Sendelauf am " & ts
end try
end friendlyRunHeader
-- =============================================================================
-- Hauptablauf
-- =============================================================================
on run
with timeout of 3600 seconds
try
-- ── Auswahl & Konfiguration ─────────────────────────────────────
set selInfo to my getSelectedUUIDs()
set selCount to item 1 of selInfo
set selectedUUIDs to item 2 of selInfo
if (count of selectedUUIDs) is 0 then error "Keine versendbaren Belege ausgewählt."
set bookingYearInfo to my getSelectedBookingYearInfo(selectedUUIDs)
set bookingYearUUID to yearUUID of bookingYearInfo
set bookingYearName to yearName of bookingYearInfo
set isOutsider to hasOutsiders of bookingYearInfo
set cfg to my loadConfig()
set my cfgConfigRecordUUID to configUUID of cfg
set my cfgMailToDefault to mailToDefault of cfg
set my cfgMailCcToDefault to mailCcToDefault of cfg
set my cfgMailSubjectPrefix to subjectPrefix of cfg
set my cfgZipPasswordDefault to zipPasswordDefault of cfg
set my cfgLogSentGroupName to logSentGroupName of cfg
set my cfgBelegeRootUUID to belegeRootUUID of cfg
set my cfgWriteDebugLog to writeDebugLog of cfg
set my cfgMailBodyFolderName to mailBodyFolderName of cfg
set my cfgMailBodyRecordName to mailBodyRecordName of cfg
set my cfgSentTagForDocs to sentTagForDocs of cfg
set my cfgDryRunPreviewPath to dryRunPreviewPath of cfg
set my cfgZipArchive to zipArchive of cfg
set my cfgMaxMailSizeMB to maxMailSizeMB of cfg
set my cfgSupportedTypes to supportedTypes of cfg
set my cfgSupportedTypesText to supportedTypesText of cfg
set my cfgStageDir to stageDir of cfg
my resetDebugLog()
my writeDebug("Script start")
my writeDebug("Config record UUID=" & my cfgConfigRecordUUID)
my writeDebug("Mode=" & my modeText() & " maxMB=" & my cfgMaxMailSizeMB & " types=" & my cfgSupportedTypesText)
my writeDebug("Booking year: yearUUID=" & bookingYearUUID & " yearName=" & bookingYearName & " isOutsider=" & isOutsider)
my writeDebug("Selected raw count=" & selCount & " usable=" & (count of selectedUUIDs))
set toSend to my filterUUIDsWithoutTag(selectedUUIDs, my cfgSentTagForDocs)
my writeDebug("ToSend count=" & (count of toSend))
if (count of toSend) is 0 then error "Alle ausgewählten Belege sind bereits als versendet markiert und werden übersprungen."
-- ── Prüfung 1: Dateitypen ───────────────────────────────────────
set typeSplit to my splitBySupportedType(toSend)
set badTypeUUIDs to item 2 of typeSplit
if (count of badTypeUUIDs) > 0 then
set badText to (count of badTypeUUIDs) & " Beleg(e) haben einen Dateityp, der nicht versendet wird." & return & ¬
"Erlaubt sind: " & my cfgSupportedTypesText & return & return & my namesListText(badTypeUUIDs, 8, false) & return & ¬
"Tipp: Fotos (JPG, HEIC, PNG) vorher in DEVONthink zu PDF konvertieren."
if (count of item 1 of typeSplit) is 0 then
display dialog windowTitle & return & return & badText buttons {"OK"} default button "OK"
return
end if
set typeChoice to button returned of (display dialog windowTitle & return & return & badText buttons {"Abbrechen", "Ohne diese weiter"} default button "Abbrechen")
if typeChoice is "Abbrechen" then
my writeDebug("User abort: unsupported types")
return
end if
set toSend to item 1 of typeSplit
end if
-- ── Prüfung 2: Größe & Aufteilung ───────────────────────────────
set budgetBytes to my attachmentBudgetBytes()
set pkgResult to my buildPackages(toSend, budgetBytes)
set pkgList to packages of pkgResult
set pkgSizes to sizes of pkgResult
set oversizedUUIDs to oversized of pkgResult
my writeDebug("Budget bytes=" & budgetBytes & " packages=" & (count of pkgList) & " oversized=" & (count of oversizedUUIDs))
if (count of oversizedUUIDs) > 0 then
set bigText to (count of oversizedUUIDs) & " Beleg(e) sind allein schon zu groß für eine Mail" & return & ¬
"(max. " & my cfgMaxMailSizeMB & " MB Mailgröße, netto ca. " & my formatMB(budgetBytes) & " MB Anhänge):" & return & return & my namesListText(oversizedUUIDs, 8, true) & return & ¬
"Tipp: PDF verkleinern oder getrennt versenden."
if (count of pkgList) is 0 then
display dialog windowTitle & return & return & bigText buttons {"OK"} default button "OK"
return
end if
set bigChoice to button returned of (display dialog windowTitle & return & return & bigText buttons {"Abbrechen", "Ohne diese weiter"} default button "Abbrechen")
if bigChoice is "Abbrechen" then
my writeDebug("User abort: oversized")
return
end if
end if
-- toSend = genau das, was in den Paketen steckt
set toSend to {}
repeat with p in pkgList
repeat with u in p
set end of toSend to (u as text)
end repeat
end repeat
set nItems to count of toSend
set nMails to count of pkgList
-- ── Modus & Eingaben ────────────────────────────────────────────
set drChoice to button returned of (display dialog windowTitle & return & return & "Modus wählen:" buttons {"Dry Run", "Senden (Entwurf erstellen)", "Abbrechen"} default button "Dry Run")
if drChoice is "Abbrechen" then
my writeDebug("User hat Abbrechen geklickt")
return
end if
set isDryRun to (drChoice is "Dry Run")
my writeDebug("Mode isDryRun=" & (isDryRun as text))
set theTo to text returned of (display dialog "Empfänger E-Mail:" default answer my cfgMailToDefault)
set theCcTo to text returned of (display dialog "CC-Empfänger (optional):" default answer my cfgMailCcToDefault)
if my cfgZipArchive then
set zipPass to text returned of (display dialog "ZIP-Passwort:" default answer my cfgZipPasswordDefault with hidden answer)
else
set zipPass to ""
end if
my writeDebug("Recipient=" & theTo)
if isDryRun then
set previewPath to my writeDryRunPreviewFile(pkgList, pkgSizes, theTo)
set dryRunText to my dryRunSummaryText(toSend, nMails, previewPath)
set dryRunChoice to button returned of (display dialog windowTitle & return & return & dryRunText buttons {"OK", "Liste im Finder zeigen"} default button "OK")
if dryRunChoice is "Liste im Finder zeigen" then
my showDryRunPreviewInFinder(previewPath)
end if
my writeDebug("Dry run end")
return
end if
set previewText to "Ausgewählt: " & selCount & return & ¬
"Zu senden (ungesendet): " & nItems & return & ¬
"Versandmodus: " & my modeText() & return & ¬
"Anzahl Mails: " & nMails & return & return & ¬
"Empfänger: " & theTo & return
if isOutsider then
set previewText to previewText & "(Außenseiter-Belege → globales Versendet-Log)" & return
else
set previewText to previewText & "Buchungsjahr: " & bookingYearName & return
end if
if my cfgZipArchive is false then
set previewText to previewText & return & "Hinweis: Belege gehen unverschlüsselt (ohne ZIP-Passwort) raus." & return
end if
if nMails is 1 then
set confirmText to "Mail-Entwurf erstellen?"
else
set confirmText to nMails & " Mail-Entwürfe erstellen?"
end if
set proceed to button returned of (display dialog previewText & return & confirmText buttons {"Abbrechen", "Weiter"} default button "Weiter")
if proceed is "Abbrechen" then
my writeDebug("User aborted before export")
return
end if
-- ── Stage vorbereiten ───────────────────────────────────────────
set bid to my batchId()
set ts to my isoTimestamp()
my writeDebug("Batch=" & bid & " ts=" & ts & " isOutsider=" & isOutsider)
set stageBase to my stageBaseDir()
my ensureFolder(stageBase)
my cleanupOldStages(stageBase)
set stageRootPOSIX to stageBase & bid & "/"
my ensureFolder(stageRootPOSIX)
my writeDebug("Stage root=" & stageRootPOSIX)
-- Mailtext einmal für alle Mails holen (Vorlagenauswahl nur einmal)
set mailBodyText to my getMailBodyTextFromCurrentDB()
if mailBodyText is "" then set mailBodyText to "Im Anhang die aktuellen Belege."
set toAddresses to my parseCSVEmails(theTo)
set ccAddresses to my parseCSVEmails(theCcTo)
my writeDebug("toAddresses=" & my joinList(toAddresses, ", ") & " ccAddresses=" & my joinList(ccAddresses, ", "))
-- je Mail: {bidK, zipName}
set mailInfos to {}
-- ── Je Paket: exportieren, ggf. zippen, Entwurf erstellen ──────
repeat with k from 1 to nMails
set pkg to item k of pkgList
if nMails is 1 then
set bidK to bid
set subjectText to my cfgMailSubjectPrefix
else
set bidK to bid & "-" & k
set subjectText to my cfgMailSubjectPrefix & " (" & k & "/" & nMails & ")"
end if
set mailDirPOSIX to stageRootPOSIX & "mail-" & k & "/"
my ensureFolder(mailDirPOSIX)
set mailDirAlias to (POSIX file mailDirPOSIX) as alias
my writeDebug("Mail " & k & "/" & nMails & " dir=" & mailDirPOSIX & " items=" & (count of pkg))
repeat with u in pkg
my exportRecordByUUID(u as text, mailDirAlias)
end repeat
do shell script "find " & quoted form of mailDirPOSIX & " -name '.DS_Store' -type f -delete"
do shell script "find " & quoted form of mailDirPOSIX & " -name '__MACOSX' -exec rm -rf {} + 2>/dev/null || true"
do shell script "find " & quoted form of mailDirPOSIX & " -name 'DEVONtech_storage' -exec rm -rf {} + 2>/dev/null || true"
set fileListText to do shell script "find " & quoted form of mailDirPOSIX & " -type f ! -name '.*' | sort"
if fileListText is "" then error "Keine exportierten Dateien gefunden (Mail " & k & ")."
if my cfgZipArchive then
set zipName to "Belege_" & bidK & ".zip"
set zipPath to stageRootPOSIX & zipName
if zipPass is "" then
do shell script "cd " & quoted form of mailDirPOSIX & " ; /usr/bin/zip -r -j " & quoted form of zipPath & " . "
else
do shell script "cd " & quoted form of mailDirPOSIX & " ; /usr/bin/zip -P " & quoted form of zipPass & " -r -j " & quoted form of zipPath & " . "
end if
set attachPaths to {zipPath}
my writeDebug("ZIP created: " & zipPath)
else
set zipName to ""
set attachPaths to paragraphs of fileListText
my writeDebug("Single files: " & (count of attachPaths))
end if
tell application "Mail"
set newMessage to make new outgoing message with properties {visible:true, subject:subjectText, content:(mailBodyText & return & return)}
tell newMessage
repeat with toAddr in toAddresses
make new to recipient at end of to recipients with properties {address:(toAddr as rich text)}
end repeat
repeat with ccAddr in ccAddresses
make new cc recipient at end of cc recipients with properties {address:(ccAddr as rich text)}
end repeat
repeat with aPath in attachPaths
make new attachment with properties {file name:(POSIX file (aPath as rich text) as alias)} at after the last paragraph
delay 0.2
end repeat
end tell
end tell
my writeDebug("Mail draft created " & k & "/" & nMails)
set end of mailInfos to {bidK, zipName}
end repeat
tell application "Mail" to activate
-- ── Logs vorbereiten ────────────────────────────────────────────
if isOutsider then
set refUUID to item 1 of toSend as text
set logGroupUUID to my getOrCreateGlobalSentGroupUUID(refUUID)
set ndjsonName to "sentlog_outsiders.ndjson"
set mdName to "sentlog_outsiders"
set mdHeader to "# Belegversand-Log (außerhalb von Buchungsjahren)" & return
else
set logGroupUUID to my getOrCreateChildGroupUUID(bookingYearUUID, my cfgLogSentGroupName)
set ndjsonName to "sentlog_" & bookingYearName & ".ndjson"
set mdName to "sentlog_" & bookingYearName
set mdHeader to "# Belegversand-Log " & bookingYearName & return
end if
set ndjsonLogUUID to my getOrCreateTextLogUUID(logGroupUUID, ndjsonName, "")
set mdLogUUID to my getOrCreateMarkdownLogUUID(logGroupUUID, mdName, mdHeader)
set resultUUID to my getOrCreateMarkdownLogUUID(logGroupUUID, "!Ergebnis", "# Letzter Sendelauf" & return)
set runHeader to my friendlyRunHeader(ts)
set tableHead to "| Zeit | Batch | Empfänger | ZIP | DEVONthink-Name | Dateiname | DEVONthink |" & return & "|---|---|---|---|---|---|---|"
set currentMdText to my getDTRecordTextByUUID(mdLogUUID)
if currentMdText is "" or currentMdText is mdHeader or currentMdText is (mdHeader & return) then
set mdRunIntro to return & "## " & runHeader & return & return & tableHead
else
set mdRunIntro to return & return & "## " & runHeader & return & return & tableHead
end if
my appendLineToDTRecordByUUID(mdLogUUID, mdRunIntro)
if my cfgZipArchive then
set modeKey to "zip"
else
set modeKey to "single"
end if
-- ── Taggen & protokollieren, je Mail ────────────────────────────
repeat with k from 1 to nMails
set pkg to item k of pkgList
set bidK to item 1 of (item k of mailInfos)
set zipName to item 2 of (item k of mailInfos)
if zipName is "" then
set zipCell to "einzeln"
else
set zipCell to "`" & my mdEscape(zipName) & "`"
end if
repeat with thisUUID in pkg
set uuidText to thisUUID as text
my addTagByUUID(uuidText, my cfgSentTagForDocs)
set recName to my getRecordNameByUUID(uuidText)
set recFileName to my getRecordFileNameByUUID(uuidText)
set recRef to my getReferenceURLByUUID(uuidText)
set j to "{\"ts\":\"" & ts & "\",\"batch\":\"" & bidK & "\",\"mail\":\"" & k & "/" & nMails & "\",\"mode\":\"" & modeKey & "\",\"to\":\"" & my jsonEscape(theTo) & "\",\"zip\":\"" & my jsonEscape(zipName) & "\",\"uuid\":\"" & my jsonEscape(uuidText) & "\",\"ref\":\"" & my jsonEscape(recRef) & "\",\"name\":\"" & my jsonEscape(recName) & "\",\"filename\":\"" & my jsonEscape(recFileName) & "\"}"
if recRef is "" then
set mdLink to ""
else
set mdLink to "[öffnen](" & recRef & ")"
end if
set mdLine to "| " & ts & " | `" & bidK & "` | " & my mdEscape(theTo) & " | " & zipCell & " | " & my mdEscape(recName) & " | " & my mdEscape(recFileName) & " | " & mdLink & " |"
my appendLineToDTRecordByUUID(ndjsonLogUUID, j)
my appendLineToDTRecordByUUID(mdLogUUID, mdLine)
my writeDebug("PROCESS DONE uuid=" & uuidText & " mail=" & k)
end repeat
end repeat
-- Stage bleibt bewusst stehen (Mail lädt Anhänge evtl. verzögert);
-- alte Läufe werden beim nächsten Start aufgeräumt.
my writeResultRecordByUUID(resultUUID, ts, bid, toAddresses, ccAddresses, nItems, nMails, isOutsider)
my showCompletionNotification(nItems, nMails)
my writeDebug("Script end - success")
on error errMsg number errNum
my writeDebug("ERROR number=" & errNum & " message=" & errMsg)
display dialog "Hinweis: " & errMsg buttons {"OK"} default button "OK"
end try
end timeout
end run