SendSMTPMail

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,
server, replyTo, authUser, authPass)
to do the heavy lifting for you. The source code for SendSMTPMail is reproduced below.

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
Posted in MWScript, Sample Code | Comments Off on SendSMTPMail