SendSMTPMail is a script handler you can implement that takes over the job of sending SMTP mail.
The handler signature is:
SendSMTPMail(recipients, attachmentPath, subject, message, attachmentFileName)
When MoneyWorks 9 has an email + attachment to send and the preferences are set to send via SMTP, the job is usually performed by Built_In:__SendSMTPMail
.
If there is a script handler in your document named SendSMTPMail
, then that handler will be called instead (note: if there are multiple such handlers, they will all be called, potentially resulting in the message being sent multiple times)
- recipients may be a single recipient or a comma-delimited list
- attachmentPath is the posix or windows path to the file to be attached
- subject is the email subject
- message is the plain text message; may be blank
- attachmentFileName is a suggested attachment name which may differ from the actual filename of the attachmentPath (e.g. "Invoice 1234.pdf")
In MoneyWorks 9, your SendSMTPMail handler can call internal handlers Built_In:__SendSMTPMail(to, path, subject, message_text, attachmentName)
or Built_In:__SendSMTPMail_WithCredentials(to, path, subject, msg, attachmentName,
to do the heavy lifting for you. The source code for SendSMTPMail is reproduced below.
server, replyTo, authUser, authPass)
Note that prior to v9, if there was no user defined handler, an external executable (sendEmail) was used. SendEmail(.exe) is no longer installed with MoneyWorks 9, so if you have scripts that rely on it, you may need to update them.
Automatically called: When a message needs to be sent via SMTP
Use for: Customising SMTP sending behaviour
Availability: MoneyWorks Gold 8.1.7 and later.
Below is the source of the built in handler (in MoneyWorks 9.0.2). You can copy/paste this into your document as a starting point, but to have it override the built-in handlers, you should remove the leading underscores from __SendSMTPMail and __SendSMTPMail_WithCredentials.
constant meta = "MoneyWorks Built-in MWScript handler library" /******************************************************************************** When MoneyWorks has an email + attachment to send and the preferences are set to send via SMTP... IF there is a script handler named SendSMTPMail, then that handler will be called to send the email. OTHERWISE, Built_In:__SendSMTPMail will be called Your SendSMTPMail handler MAY call Built_In:__SendSMTPMail or Built_In:__SendSMTPMail_WithCredentials handler signature: ------------------------------------------------------------------------------ SendSMTPMail(recipients, attachmentPath, subject, message, attachmentFileName) ------------------------------------------------------------------------------ - recipients may be a single recipient or a comma-delimited list - attachmentPath is the posix or windows path to the file to be attached - subject is the email subject - message is the plain text message; may be blank - attachmentFileName is a suggested attachment name which may differ from the actual filename of the attachmentPath (e.g. "Invoice 1234.pdf") **********************************************************************************/ property TLS_Enabled = true // Build the email message in this proprty property payload // Use Preload_Built_In:SetVerbose(1) to get CURL logging output in the log file to debug connections property verbose = 0 // Callback to deliver payload to libcurl; don't return more than the requested number of bytes property pl_offset // Callback to deliver payload to libcurl; don't return more than the requested number of bytes on Read_Payload_Callback(bytes, data) let thispayload = Mid(payload, pl_offset + 1, bytes, IN_BYTES) let pl_offset = pl_offset + bytes return thispayload end on Curl_WriteHandler(bytes, data) end // Break lines between words on GetLineFromMessage(m) // Try to break at <= 78 characters (although RFC 821 allows up to 1000, it recommends 78 + \r\n) let line = Left(m, 78) let nl = Position(line, "\r") if nl == 0 let nl = Position(line, "\n") endif if nl <> 0 return Left(line, nl - 1) + " " // include space to indicate that the linebreak is a soft one endif if Length(line) == 78 while Right(line, 1) <> " " and Position(line, " ") > 0 let line = Left(line, Length(line) - 1) endwhile endif return line end on AppendPayload(line) let payload = payload + line + "\r\n" end on CleanEmail(ad) let lt = PositionInText(ad, "<") let gt = PositionInText(ad, ">") if lt and gt and gt > lt return Trim(Mid(ad, lt + 1, gt - lt - 1), true) endif return trim(ad, true) end on AttachFileWithPath(path, attachmentName, MIME_bound) let content_type = "application/octet-stream" if Right(path, 4) = ".pdf" let content_type = "application/pdf" elseif Right(path, 4) = ".jpg" let content_type = "image/jpeg" elseif Right(path, 4) = ".png" let content_type = "image/png" endif AppendPayload("--" + MIME_bound) if attachmentName != "" AppendPayload("Content-Type: " + content_type + "; name=\"" + attachmentName + "\"") else AppendPayload("Content-Type: " + content_type) endif AppendPayload("Content-Transfer-Encoding: base64") AppendPayload("") let fd = File_Open(path) let b64 = Base64Encode(File_Read(fd), true) File_Close(fd) AppendPayload(b64) end on __SendSMTPMail_WithCredentials(to, path, subject, message_text, attachmentName, server, replyTo, authUser, authPass, bearer) public let res = "" /* * Mail headers */ // let to = CleanEmail(to) to may contain multiple comma-delimited addresses let payload = "" AppendPayload("Date: " + DateToText(Time(), DateFormRFC2822)) // send To: all recipients. Could sort them into To:, CC:, BCC: if you want, using some criteria // (maybe if the address is prefixed with cc: or bcc:, parse that) AppendPayload("To: " + to) AppendPayload("From: " + replyTo) AppendPayload("Reply-To: " + CleanEmail(replyTo)) AppendPayload("Message-ID: <" + MakeGUID() + "@" + server + ">") AppendPayload("Subject: " + subject) AppendPayload("User-Agent: MoneyWorks/"+version+"_SendSMTPMail_libcurl") /* * MIME header */ AppendPayload("MIME-Version: 1.0") let MIME_bound = "MW-"+MakeGUID() AppendPayload("Content-Type: multipart/mixed; boundary=" + MIME_bound) AppendPayload("") AppendPayload("This is a message with multiple parts in MIME format.") AppendPayload("--" + MIME_bound) /* * Add the text/plain MIME section with the message */ AppendPayload("Content-Transfer-Encoding: quoted-printable") AppendPayload("Content-Type: text/plain; charset=utf-8; format=fixed") AppendPayload("") // linebreak the message (chunk_split() fn would be nice) let mtext = message_text while length(mtext) let a_line = GetLineFromMessage(mtext) // convert the line to quoted-printable // passing = as delim to URLEncode will only encode non-printable-ASCII and = AppendPayload(URLEncode(a_line, "=")) let mtext = Right(mtext, Length(mtext) - Length(a_line)) endwhile AppendPayload(" ") //syslog(payload) /* * Add the attachment, with the suggested attachment name */ if TypeOf(path) == TypeText and path != "" and TypeOf(attachmentName) == TypeText AttachFileWithPath(path, attachmentName, MIME_bound) elseif TypeOf(path) == TypeArray // array of ["Attachment Name"] = "filepath" foreach k in array path let att_name = "" if TypeOf(attachmentName) = TypeArray and ElementExists(attachmentName, k) let att_name = attachmentName[k] endif AttachFileWithPath(path[k], att_name, MIME_bound) endfor endif /* * MIME end marker */ AppendPayload("--" + MIME_bound + "--") /* * Now use CURL to send... */ let curl = curl_init() if PositionInText(server, ":465") curl_setopt(curl, CURLOPT_URL, "smtps://" + server) else curl_setopt(curl, CURLOPT_URL, "smtp://" + server) endif if TLS_Enabled curl_setopt(curl, CURLOPT_USE_SSL, 1) else curl_setopt(curl, CURLOPT_USE_SSL, 0) endif curl_setopt(curl, CURLOPT_VERBOSE, verbose) if authUser != "" curl_setopt(curl, CURLOPT_USERNAME, authUser) if TypeOf(bearer) = TypeText and bearer != "" curl_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BEARER); curl_setopt(curl, CURLOPT_XOAUTH2_BEARER, bearer); else curl_setopt(curl, CURLOPT_PASSWORD, authPass) endif endif /* Note that this option isn't strictly required, omitting it will result * in libcurl sending the MAIL FROM command with empty sender data. All * autoresponses should have an empty reverse-path, and should be directed * to the address in the reverse-path which triggered them. Otherwise, * they could cause an endless loop. See RFC 5321 Section 4.5.5 for more * details. */ curl_setopt(curl, CURLOPT_MAIL_FROM, CleanEmail(replyTo)) /* Iterate over comma-separated recipient list (an explode() fn would be nice) * It is irrelevant which are To, CC, BCC — that cosmetic information is provided in headers. */ let recipients = CreateArray() let i = 0 if verbose syslog("recipients text: " + to) endif foreach rcpt in text to // // The recipient must be a pure email address; extract that if we have "Name <addr>" format // (the foreach in text will already do an implicit trim of whitespace) let r = CleanEmail(rcpt) if r <> "" let recipients[i] = r let i = i + 1 if verbose syslog("Adding recipient: " + r) endif endif endfor if CountElements(recipients) = 0 syslog("[ERROR] no recipients for message") return endif if GetAppPreference("bccToSelf") let recipients[i] = CleanEmail(GetAppPreference("replyTo")) syslog("added bcc to " + recipients[i]) endif curl_setopt(curl, CURLOPT_MAIL_RCPT, recipients) /* We're using a callback function to specify the payload (the headers and * body of the message). You could just use the CURLOPT_READDATA option to * specify a FILE pointer to read from. */ curl_setopt(curl, CURLOPT_READFUNCTION, "Read_Payload_Callback") curl_setopt(curl, CURLOPT_UPLOAD, 1) curl_setopt(curl, CURLOPT_WRITEFUNCTION, "Curl_WriteHandler") // Having this gets us a numeric curlcode as a result from curl_exec if TLS_Enabled curl_setopt(curl, CURLOPT_SSL_OPTIONS, 2) // = CURLSSLOPT_NO_REVOKE Windows needs this for gmail endif let pl_offset = 0 /* Send the message */ let res = curl_exec(curl); curl_close(curl); // the text "[INFO] Email was sent successfully" MUST be logged for MW to consider the operation succeeded if(res == 0) syslog("[INFO] Email was sent successfully to " + to) else syslog("[ERROR] " + curl_strerror(res)) endif end constant fNoOverrideReplyTo = #0004 on OverrideReplyTo(reply) if Initials <> "" let usersettings = Find("Login.Email+`\t`+flags", "initials=`"+Initials+"`", 1) if usersettings <> "" let f = TextToNum(Slice(usersettings, 2, "\t")) if TestFlags(f, fNoOverrideReplyTo) = 0 let r = Slice(usersettings, 1, "\t") if r <> "" let reply = r endif endif endif endif return reply end uses MW_SMTP_OAuth:GetBearerToken on __SendSMTPMail(to, path, subject, message_text, attachmentName) public /* * Get the server settings from preferences; * * These could be overridden (in particular the From: might want to be overridden with the logged-in user, * NOTE that the SMTP server must accept email from that user; most servers won't accept * email from addresses in other domains. There is no magical way to change that.) */ let bearerToken = "" let authPass = "" let server = GetAppPreference("smtp") let replyTo = GetAppPreference("replyTo") if GetAppPreference("useAuth") = 1 let authUser = GetAppPreference("smtpUser") let authPass = GetAppPreference("smtpPass") elseif GetAppPreference("useAuth") = 2 let authUser = GetAppPreference("smtpUser") let bearerToken = MW_SMTP_OAuth:GetBearerToken() else let authUser = "" let authPass = "" endif let replyTo = OverrideReplyTo(replyTo) __SendSMTPMail_WithCredentials(to, path, subject, message_text, attachmentName, server, replyTo, authUser, authPass, bearerToken) end on SetVerbose(v) public let verbose = v end on SetSMTPEnableTLS(b) public let TLS_Enabled = b end