(* _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 "-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