• Publicity: Public Only All

acs-mail-lite-procs.tcl

Provides a simple API for reliably sending email.

Location:
packages/acs-mail-lite/tcl/acs-mail-lite-procs.tcl
Created:
22 March 2002
Author:
Eric Lorenzo <eric@openforce.net>
CVS Identification:
$Id: acs-mail-lite-procs.tcl,v 1.108.2.34 2024/07/30 08:55:29 antoniop Exp $

Procedures in this file

Detailed information

acs_mail_lite::configured_p (public)

 acs_mail_lite::configured_p

Determine, whether the outgoing SMTPHost is configured and reachable.

Partial Call Graph (max 5 caller/called nodes):
%3 acs_mail_lite::get_delivery_parameters acs_mail_lite::get_delivery_parameters (private) acs_mail_lite::configured_p acs_mail_lite::configured_p acs_mail_lite::configured_p->acs_mail_lite::get_delivery_parameters

Testcases:
No testcase defined.

acs_mail_lite::get_parameter (public, deprecated)

 acs_mail_lite::get_parameter -name name [ -default default ]
Deprecated. Invoking this procedure generates a warning.

Returns an apm-parameter value of this package Deprecated: just a wrapper for parameter::get

Switches:
-name (required)
-default (optional)
Options:
-name
parameter name
-default
default parameter value
Returns:
apm-parameter value of this package
See Also:

Partial Call Graph (max 5 caller/called nodes):
%3 acs_mail_lite::get_package_id acs_mail_lite::get_package_id (private) ad_log_deprecated ad_log_deprecated (public) parameter::get parameter::get (public) acs_mail_lite::get_parameter acs_mail_lite::get_parameter acs_mail_lite::get_parameter->acs_mail_lite::get_package_id acs_mail_lite::get_parameter->ad_log_deprecated acs_mail_lite::get_parameter->parameter::get

Testcases:
No testcase defined.

acs_mail_lite::send (public)

 acs_mail_lite::send [ -send_immediately ] [ -valid_email ] \
    -to_addr to_addr -from_addr from_addr [ -subject subject ] \
    -body body [ -mime_type mime_type ] [ -cc_addr cc_addr ] \
    [ -bcc_addr bcc_addr ] [ -reply_to reply_to ] \
    [ -package_id package_id ] [ -no_callback ] [ -file_ids file_ids ] \
    [ -filesystem_files filesystem_files ] \
    [ -delete_filesystem_files ] [ -extraheaders extraheaders ] \
    [ -use_sender ] [ -object_id object_id ]

Prepare an email to be sent. Various email attributes can be specified, such as subject, body, senders, recipients, attachments and so on. The proc relies on MIME and SMTP.

Switches:
-send_immediately (optional, boolean)
The email is send immediately and not stored in the acs_mail_lite_queue
-valid_email (optional, boolean)
-to_addr (required)
List of e-mail addresses to send this mail to.
-from_addr (required)
E-Mail address of the sender.
-subject (optional)
of the email
-body (required)
Text body of the email
-mime_type (optional, defaults to "text/plain")
MIME Type of the mail to send out. Can be "text/plain", "text/html".
-cc_addr (optional)
List of CC Users e-mail addresses to send this mail to.
-bcc_addr (optional)
List of CC Users e-mail addresses to send this mail to.
-reply_to (optional)
-package_id (optional)
Package ID of the sending package
-no_callback (optional, boolean)
Boolean that indicates if callback should be executed or not. If you don't provide it it will execute callbacks
-file_ids (optional)
List of file ids (items or revisions) to be sent as attachments. This will only work with files stored in the file-storage.
-filesystem_files (optional)
List of regular files on the filesystem to be sent as attachments.
-delete_filesystem_files (optional, boolean)
-extraheaders (optional)
List of keywords and their values passed in for headers. Interesting ones are: "Precedence: list" to disable autoreplies and mark this as a list message. This is as list of lists !!
-use_sender (optional, boolean)
Boolean indicating that from_addr should be used regardless of fixed-sender parameter
-object_id (optional)

Partial Call Graph (max 5 caller/called nodes):
%3 test_email_send email_send (test acs-mail-lite) acs_mail_lite::send acs_mail_lite::send test_email_send->acs_mail_lite::send acs_mail_lite::get_package_id acs_mail_lite::get_package_id (private) acs_mail_lite::send->acs_mail_lite::get_package_id acs_mail_lite::send_immediately acs_mail_lite::send_immediately (private) acs_mail_lite::send->acs_mail_lite::send_immediately db_dml db_dml (public) acs_mail_lite::send->db_dml parameter::get parameter::get (public) acs_mail_lite::send->parameter::get acs_admin::check_expired_certificates acs_admin::check_expired_certificates (private) acs_admin::check_expired_certificates->acs_mail_lite::send acs_mail_lite::check_bounces acs_mail_lite::check_bounces (private) acs_mail_lite::check_bounces->acs_mail_lite::send acs_messaging_process_queue acs_messaging_process_queue (private) acs_messaging_process_queue->acs_mail_lite::send auth::local::password::ChangePassword auth::local::password::ChangePassword (private) auth::local::password::ChangePassword->acs_mail_lite::send auth::local::password::RetrievePassword auth::local::password::RetrievePassword (private) auth::local::password::RetrievePassword->acs_mail_lite::send

Testcases:
email_send

ns_sendmail (public, deprecated)

 ns_sendmail to from subject body [ extraheaders ] [ bcc ]
Deprecated. Invoking this procedure generates a warning.

Replacement for ns_sendmail for backward compatibility.

Parameters:
to (required)
from (required)
subject (required)
body (required)
extraheaders (optional)
bcc (optional)

Partial Call Graph (max 5 caller/called nodes):
%3 acs_mail_lite::send acs_mail_lite::send (public) ad_log_deprecated ad_log_deprecated (public) ns_sendmail ns_sendmail ns_sendmail->acs_mail_lite::send ns_sendmail->ad_log_deprecated

Testcases:
No testcase defined.
[ hide source ] | [ make this the default ]

Content File Source

ad_library {

    Provides a simple API for reliably sending email.

    @author Eric Lorenzo (eric@openforce.net)
    @creation-date 22 March 2002
    @cvs-id $Id: acs-mail-lite-procs.tcl,v 1.108.2.34 2024/07/30 08:55:29 antoniop Exp $
}

package require mime 1.4
package require smtp 1.4
# package require base64 2.3.1

namespace eval acs_mail_lite {

    ad_proc -private get_package_id {} {
        @return package_id of this package
    } {
        return [apm_package_id_from_key acs-mail-lite]
    }

    d_proc -deprecated -public get_parameter {
        -name:required
        {-default ""}
    } {
        Returns an apm-parameter value of this package

        Deprecated: just a wrapper for parameter::get

        @option name parameter name
        @option default default parameter value
        @return apm-parameter value of this package
        @see parameter::get
    } {
        return [parameter::get \
                    -package_id [get_package_id] \
                    -parameter $name \
                    -default $default]
    }

    ad_proc -private mail_dir {} {
        @return incoming mail directory to be scanned for bounces
    } {
        return [parameter::get \
                    -package_id [get_package_id] \
                    -parameter "BounceMailDir" \
                    -default ""]
    }

    #---------------------------------------
    d_proc -private parse_email_address {
        -email:required
    } {
        Extracts the email address out of a mail address
        (like Joe User &lt;joe@user.com&gt;)

        @option email mail address to be parsed
        @return only the email address part of the mail address
    } {
        if {![regexp {<([^>]*)>} $email all clean_email]} {
            return $email
        } else {
            return $clean_email
        }
    }


    #---------------------------------------
    d_proc -private log_mail_sending {
        -user_id:required
    } {
        Logs mail sending time for user
        @option user_id user for whom email sending should be logged
    } {
        db_dml record_mail_sent {}
        if {![db_resultrows]} {
            db_dml insert_log_entry {}
        }
    }


    #---------------------------------------
    d_proc -private generate_message_id {
    } {
        Generate an id suitable as a Message-Id: header for an email.
        @return valid message-id for mail header
    } {
        # The combination of high resolution time and random
        # value should be pretty unique.

        return "<[clock clicks].[ns_time].oacs@[address_domain]>"
    }

    #---------------------------------------
    d_proc -private valid_signature {
        -signature:required
        -message_id:required
    } {
        Validates if provided signature syntactically matches message_id
        as generated by OpenACS and equals to the hash of the provided signature.
        GN: not sure, why both is checked.

        @param signature signature to be checked
        @param message_id message ID that the signature should be checked against
        @return boolean 0 or 1
    } {
        if {![regexp "(<\[\-0-9\]+\\.\[0-9\]+\\.oacs@[address_domain]>)" $message_id match id]
            || $signature ne [ns_sha1 $id]
        } {
            # either couldn't find message-id or signature doesn't match
            return 0
        }
        return 1
    }

    #---------------------------------------
    ad_proc -private get_delivery_parameters {} {
        Get the SMTP Parameters and return these as a dict.

        @return dict with keys identical to the package parameters
    } {
        set mail_package_id [get_package_id]

        set smtpHost [parameter::get -parameter "SMTPHost" \
                      -package_id $mail_package_id \
                      -default [ns_config ns/parameters mailhost]]
        if {$smtpHost eq ""} {
            set smtpHost localhost
        }

        set timeout [parameter::get -parameter "SMTPTimeout" \
                         -package_id $mail_package_id \
                         -default  [ns_config ns/parameters smtptimeout]]
        if {$timeout eq ""} {
            set timeout 60
        }

        set smtpPort [parameter::get -parameter "SMTPPort" \
                          -package_id $mail_package_id \
                          -default 25]

        set smtpUser [parameter::get -parameter "SMTPUser" \
                          -package_id $mail_package_id]

        set smtpPassword [parameter::get -parameter "SMTPPassword" \
                              -package_id $mail_package_id]

        set deliveryMode [parameter::get \
                               -package_id $mail_package_id \
                               -parameter EmailDeliveryMode \
                               -default default]
        return [list \
                    SMTPHost $smtpHost \
                    SMTPTimeout $timeout \
                    SMTPPort $smtpPort \
                    SMTPUser $smtpUser \
                    SMTPPassword $smtpPassword \
                    EmailDeliveryMode $deliveryMode]
    }


    #---------------------------------------
    d_proc -private smtp {
        -multi_token:required
        -headers:required
        -originator:required
        -delivery_dict:required
    } {
        Send messages via SMTP

        @param multi_token Multi Token generated which is passed
               directly to smtp::sendmessage

        @param headers List of list of header key-value pairs like
               {{from malte@cognovis.de} {to malte@cognovis.de}}

        @param delivery_dict dictionary of delivery parameters
               including the SMTP* configuration parameters

    } {

        # Consider adding code here to
        # set orignator to acs-mail-lite parameter FixedSenderEmail
        # if FixedSenderEmail is not empty,
        # so as to be consistent for all cases calling this proc.

        ns_log notice "SMTP call sendmessage <$originator>"
        set cmd [list smtp::sendmessage $multi_token -originator $originator]
        ns_log notice "SMTP call sendmessage <$originator> DONE"
        foreach header $headers {
            lappend cmd -header $header
        }
        lappend cmd \
            -servers [dict get $delivery_dict SMTPHost] \
            -ports [dict get $delivery_dict SMTPPort]

        #
        # Request authentication only, when user AND password are
        # specified. If only one of these is specified, issue a
        # warning and ignore the parameter.
        #
        set smtpUser [dict get $delivery_dict SMTPUser]
        set smtpPassword [dict get $delivery_dict SMTPPassword]
        if {$smtpUser ne "" && $smtpPassword ne "" } {
            lappend cmd -username $smtpUser -password $smtpPassword
        } elseif {$smtpUser ne ""|| $smtpPassword ne ""} {
            ns_log warning "acs-mail-lite::smtp: invalid parameter combination;\
                when SMTPUser is specified, SMTPPassword has to be provided as well and vice versa"
        }
        ns_log notice "SMTP call <$cmd>"

        ns_log Debug "send cmd: $cmd"
        if {[catch $cmd errorMsg]} {
            ns_log Error "acs-mail-lite::smtp: error $errorMsg while executing\n$cmd"
            error $errorMsg
        }
    }

    #---------------------------------------
    d_proc -private get_address_array {
        -addresses:required
    } {
        Checks if passed variable is already an array of emails,
        user_names and user_ids. If not, get the additional data
        from the db and return the full array.
        @option addresses variable to checked for array
        @return array of emails, user_names and user_ids to be used
        for the mail procedures
    } {
        if {[catch {array set address_array $addresses}]
            || [lsort [array names address_array]] ne [list email name user_id]
        } {
            #
            # Either user just passed a normal address-list or
            # user passed an array, but forgot to provide user_ids
            # or user_names, so we have to get this data from the db
            #
            if {![info exists address_array(email)]} {
                # so user passed on a normal address-list
                set address_array(email) $addresses
            }

            set address_list [list]
            foreach email $address_array(email) {
                # strip out only the emails from address-list
                lappend address_list [string tolower [parse_email_address -email $email]]
            }

            array unset address_array
            # now get the user_names and user_ids
            foreach email $address_list {
                set email [string tolower $email]
                if {[db_0or1row get_user_name_and_id {
                    select person_id as user_id, first_names || ' ' || last_name as user_name
                    from parties, persons
                    where email = :email
                      and party_id = person_id
                     order by party_id desc
                    fetch first 1 rows only
                }]} {
                    lappend address_array(email) $email
                    lappend address_array(name) $user_name
                    lappend address_array(user_id) $user_id
                } else {
                    lappend address_array(email) $email
                    lappend address_array(name) ""
                    lappend address_array(user_id) ""
                }
            }
        }
        return [array get address_array]
    }

    #---------------------------------------
    d_proc -public send {
        -send_immediately:boolean
        -valid_email:boolean
        -to_addr:required
        -from_addr:required
        {-subject ""}
        -body:required
        {-mime_type "text/plain"}
        {-cc_addr ""}
        {-bcc_addr ""}
        {-reply_to ""}
        {-package_id ""}
        -no_callback:boolean
        {-file_ids ""}
        {-filesystem_files ""}
        -delete_filesystem_files:boolean
        {-extraheaders ""}
        -use_sender:boolean
        {-object_id ""}
    } {

        Prepare an email to be sent. Various email attributes can be
        specified, such as subject, body, senders, recipients,
        attachments and so on. The proc relies on MIME and SMTP.

        @param send_immediately The email is send immediately and not
               stored in the acs_mail_lite_queue

        @param to_addr List of e-mail addresses to send this mail to.

        @param from_addr E-Mail address of the sender.

        @param subject of the email

        @param body Text body of the email

        @param cc_addr List of CC Users e-mail addresses to send this mail to.

        @param bcc_addr List of CC Users e-mail addresses to send this mail to.

        @param package_id Package ID of the sending package

        @param file_ids List of file ids (items or revisions) to be sent as attachments. This will only work with files stored in
               the file-storage.

        @param filesystem_files List of regular files on the
               filesystem to be sent as attachments.

        @param delete_filesystem_files_p Decides if we want files specified
               by the 'file' parameter to be deleted once sent.

        @param mime_type MIME Type of the mail to send out. Can be
               "text/plain""text/html".

        @param extraheaders List of keywords and their values passed
               in for headers. Interesting ones are: "Precedence:
               list" to disable autoreplies and mark this as a list
               message. This is as list of lists !!

        @param no_callback Boolean that indicates if callback should
               be executed or not. If you don't provide it it will
               execute callbacks

        @param use_sender Boolean indicating that from_addr should be
               used regardless of fixed-sender parameter

    } {
        #
        # Check, if send_immediately is set if not, take global
        # parameter.
        #
        if { !$send_immediately_p } {
            set send_immediately_p [parameter::get \
                                        -package_id [get_package_id] \
                                        -parameter "send_immediately" \
                                        -default 0]
        }

        #
        # If send_immediately_p true, then start
        # acs_mail_lite::send_immediately, so mail is not stored in
        # the db before delivery.
        #
        if { $send_immediately_p } {
            acs_mail_lite::send_immediately \
                -to_addr $to_addr \
                -cc_addr $cc_addr \
                -bcc_addr $bcc_addr \
                -from_addr $from_addr \
                -reply_to $reply_to \
                -subject $subject \
                -body $body \
                -package_id $package_id \
                -file_ids $file_ids \
                -filesystem_files $filesystem_files \
                -delete_filesystem_files_p $delete_filesystem_files_p \
                -mime_type $mime_type \
                -no_callback_p $no_callback_p \
                -extraheaders $extraheaders \
                -use_sender_p $use_sender_p \
                -object_id $object_id
        } else {
            # else, store it in the db and let the sweeper deliver the mail
            set creation_date [clock format [clock seconds] -format "%Y.%m.%d %H:%M:%S"]
            set locking_server ""
            db_dml create_queue_entry {}
        }
    }


    #---------------------------------------
    ad_proc -private sweeper {} {
        Send messages in the acs_mail_lite_queue table.
    } {
        # Make sure that only one thread is processing the queue at a time.
        if {[nsv_incr acs_mail_lite send_mails_p] != 1} {
            return
        }

        ad_try {
            db_foreach get_queued_messages {} {
                # Lock the message and also make sure that it was not
                # locked or deleted by somebody else in the meantime.
                set locking_server [ad_url]
                db_dml lock_queued_message {}

                if {[db_resultrows] == 1} {
                    # send the mail
                    ad_try {
                        acs_mail_lite::send_immediately \
                            -to_addr $to_addr \
                            -cc_addr $cc_addr \
                            -bcc_addr $bcc_addr \
                            -from_addr $from_addr \
                            -reply_to $reply_to \
                            -subject $subject \
                            -body $body \
                            -package_id $package_id \
                            -object_id $object_id \
                            -file_ids $file_ids \
                            -filesystem_files $filesystem_files \
                            -delete_filesystem_files_p $delete_filesystem_files_p \
                            -mime_type $mime_type \
                            -no_callback_p $no_callback_p \
                            -extraheaders $extraheaders \
                            -use_sender_p $use_sender_p
                    } on error {errorMsg} {
                        ad_log Error "Could not send queued mail (message $id): $errorMsg"
                        # Uncommenting the following two lines would
                        # cause failed emails to be retried at the
                        # next sweep. This could make sense only for
                        # certain kinds of errors, so it is best left
                        # commented and delegated to e.g. downstream
                        # callbacks.
                        ## set locking_server ""
                        ## db_dml lock_queued_message {}
                    } on ok {r} {
                        # mail was sent, delete the queue entry
                        db_dml delete_queue_entry {}
                    }
                }
            }
        } finally {
            nsv_unset acs_mail_lite send_mails_p
        }
    }

    ad_proc -private encode_email_address {email} {
        set d [lindex [::mime::parseaddress $email] 0]
        dict with d {
            if {$phrase ne "" && ![string is ascii -strict $phrase]} {
                set email "[mime::word_encode utf-8 quoted-printable $phrase] <$address>"
            }
        }
        return $email
    }

    #---------------------------------------
    d_proc -private send_immediately {
        {-valid_email_p "0"}
        -to_addr:required
        {-cc_addr ""}
        {-bcc_addr ""}
        -from_addr:required
        {-reply_to ""}
        {-subject ""}
        -body:required
        {-package_id ""}
        {-file_ids ""}
        {-filesystem_files ""}
        {-delete_filesystem_files_p "0"}
        {-mime_type "text/plain"}
        {-no_callback_p "0"}
        {-extraheaders ""}
        {-use_sender_p "0"}
        {-object_id ""}
        {-force_delivery_mode ""}
    } {

        Prepare an email to be sent immediately. Various email
        attributes can be specified, such as subject, body, senders,
        recipients, attachments and so on. The proc relies on MIME and
        SMTP.

        @param to_addr List of e-mail addresses to send this mail to.

        @param from_addr E-Mail address of the sender.

        @param reply_to E-Mail address to which replies should
               go. Defaults to from_addr

        @param subject of the email

        @param body Text body of the email

        @param cc_addr List of CC Users e-mail addresses to send this
               mail to.

        @param bcc_addr List of CC Users e-mail addresses to send this
               mail to.

        @param package_id Package ID of the sending package

        @param file_ids List of file ids (items or revisions) to be sent as attachments. This will only work with files
               stored in the file-storage.

        @param filesystem_files List of regular files on the
               filesystem to be sent as attachments.

        @param delete_filesystem_files_p Decides if we want files
               specified by the 'file' parameter to be deleted once
               sent.

        @param mime_type MIME Type of the mail to send out. Can be
               "text/plain""text/html".

        @param extraheaders List of keywords and their values passed
               in for headers. Interesting ones are: "Precedence:
               list" to disable autoreplies and mark this as a list
               message. This is as list of lists !!

        @param no_callback_p Indicates if callback should be executed
               or not. If you don't provide it it will execute
               callbacks.

        @param use_sender_p Boolean indicating that from_addr should
               be used regardless of fixed-sender parameter

        @param object_id Object id that caused this email to be sent

        @param force_delivery_mode Force the specified delivery mode
               for this single call
    } {

        set mail_package_id [get_package_id]
        if {$package_id eq ""} {
            set package_id $mail_package_id
        }

        # Decide which sender to use
        set fixed_sender [parameter::get \
                              -parameter "FixedSenderEmail" \
                              -package_id $mail_package_id]

        if { $fixed_sender ne "" && !$use_sender_p} {
            set from_addr $fixed_sender
        }

        set from_addr [encode_email_address $from_addr]
        set to_addr [lmap email $to_addr {encode_email_address $email}]

        # Set the Reply-To
        if {$reply_to eq ""} {
            set reply_to $from_addr
        }

        # Get any associated data indicating need to sign message-id

        # Recipients might be specified as "DisplayName <email>". This
        # format is valid for the "To:" email header, but not for the
        # "RCPT TO", hence we strip the eventually-present display
        # name from the latter.
        set to_addr_header $to_addr
        set to_addr [list]
        foreach addr $to_addr_header {
            if {[regexp {^.* <(.*)>$} $addr _ email]} {
                lappend to_addr $email
            } else {
                lappend to_addr $addr
            }
        }

        # associate a user_id
        set rcpt_id 0
        if { [llength $to_addr] == 1 } {
            set rcpt_id [party::get_by_email -email $to_addr]
            if {$rcpt_id eq ""} {
                set rcpt_id 0
            }
        }


        # Set the message_id
        # message-id gets signed if parameter defaults not passed
        set message_id [acs_mail_lite::unique_id_create \
                            -object_id $object_id \
                            -package_id $package_id \
                            -party_id $rcpt_id]


        # Set originator header
        set originator_email [parameter::get \
                                  -parameter "OriginatorEmail" \
                                  -package_id $mail_package_id]

        # Decision based firstly on parameter,
        # and then on other values that most likely could be substituted
        # with initial choice, and while meeting definition
        # of originator header according to RFC 2822 section 3.6.2
        # https://tools.ietf.org/html/rfc2822#section-3.6.2
        # A value must be provided.
        switch -exact -- $originator_email {
            fixed_sender {
                if { $fixed_sender ne "" } {
                    set originator $fixed_sender
                } elseif$from_addr ne "" } {
                    set originator $from_addr
                } else {
                    set originator $message_id
                }
            }
            from_address {
                if { $from_addr ne "" } {
                    set originator $from_addr
                } elseif$fixed_sender ne "" } {
                    set originator $fixed_sender
                } else {
                    set originator $message_id
                }
            }
            message_id {
                set originator $message_id
            }
            reply_to {
                if { $reply_to ne "" } {
                    set originator $reply_to
                } elseif$from_addr ne "" } {
                    set originator $from_addr
                } else {
                    set originator $message_id
                }
            }
            bounce_address -
            default {
                # Build the originator address to be used as envelope sender
                # and originator etc.
                set originator [bounce_address -user_id $rcpt_id \
                                   -package_id $package_id \
                                   -message_id $message_id]
            }
        }

        ns_log notice "ORIGINATOR <$originator>"
        # Set the date
        set message_date [acs_mail_lite::utils::build_date]

        # Build the message body
        set tokens [acs_mail_lite::utils::build_body \
                        -mime_type $mime_type -- $body]

        # Add attachments if any
        # ...from file-storage
        if {$file_ids ne ""} {
            set item_ids [list]

            # Check if we are dealing with revisions or items.
            foreach file_id $file_ids {
                set item_id [content::revision::item_id -revision_id $file_id]
                if {$item_id eq ""} {
                    lappend item_ids $file_id
                } else {
                    lappend item_ids $item_id
                }
            }

            db_foreach get_file_info {} {
                lappend tokens [mime::initialize \
                                    -param [list name [ns_quotehtml $title]] \
                                    -header [list Content-Disposition "attachment; filename=\"$name\""] \
                                    -header [list Content-Description $title] \
                                    -canonical $mime_type \
                                    -file [content::revision::get_cr_file_path -revision_id $revision_id]]
            }
        }

        # ...from filesystem
        if {$filesystem_files ne ""} {
            # get root of folders into which files are allowed to be sent
            set filesystem_attachments_root [parameter::get \
                                                 -parameter "FilesystemAttachmentsRoot" \
                                                 -package_id $mail_package_id \
                                                 -default ""]
            if {$filesystem_attachments_root eq ""} {
                # on a unix system this could be '/tmp'
                set filesystem_attachments_root [ad_tmpdir]
            }
            foreach f $filesystem_files {
                # make the filename absolute
                if {[file pathtype $f] ne "absolute"} {
                    set f [file join [pwd$f]
                }
                if {![file exists $f]} {
                    ns_log Error "acs-mail-lite::send: Could not send mail: file '$f' does not exist"
                    return
                }
                if {[string first $filesystem_attachments_root $f] != 0} {
                    ns_log Error "acs-mail-lite::send: Could not send mail: file '$f'\
                           is outside the allowed root folder for attachments\
                           '$filesystem_attachments_root'"
                    return
                }
                set name [file tail $f]
                set mime_type [cr_filename_to_mime_type $name]
                lappend tokens [mime::initialize \
                                    -param [list name $name] \
                                    -header [list Content-Disposition "attachment; filename=\"$name\""] \
                                    -header [list Content-Description $name] \
                                    -canonical $mime_type \
                                    -file $f]
            }
        }

        if {$file_ids ne "" || $filesystem_files ne ""} {
            set tokens [mime::initialize -canonical "multipart/mixed" -parts $tokens]
        }

        ### Add the headers

        mime::setheader $tokens "message-id" $message_id
        mime::setheader $tokens date $message_date

        # Set the subject
        if { $subject ne "" } {
            set encoded_subject [acs_mail_lite::utils::build_subject -- $subject]
            mime::setheader $tokens Subject $encoded_subject
        }

        # Add extra headers
        foreach header $extraheaders {
            mime::setheader $tokens [lindex $header 0] [lindex $header 1]
        }

        # Get the delivery parameters, including SMTP
        set deliveryDict [get_delivery_parameters]

        # Rollout support
        set default_send_mode smtp

        if {$force_delivery_mode ne ""} {
            set delivery_mode $force_delivery_mode
        } else {
            set delivery_mode [dict get $deliveryDict EmailDeliveryMode]
        }

        foreach w $delivery_mode {
            if {$w ni {smtp nssmtpd default log redirect ignore}} {
                ns_log warning "unexpected entry '$w' in parameter EmailDeliveryMode (ignored)"
            }
        }

        if {"nssmtpd" in $delivery_mode} {
            #
            # Filter the word "nssmtpd" from the EmailDeliveryMode and
            # try to use "nssmtpd" as default_send mode
            #
            if {[llength $delivery_mode] > 1} {
                # Filter "nssmtpd" from the list
                set delivery_mode [lmap m $delivery_mode {
                    if {$m eq "nssmtpd"} continue
                    set m
                }]
            }
            #
            # "ns_smtpd" can be used, when it is available and no
            # password mode is specified.
            #
            if { [namespace which ns_smtpd] eq ""
                 || [dict get $deliveryDict SMTPPassword] ne ""
                 || [dict get $deliveryDict SMTPUser] ne ""
             } {
                ns_log warning "configured 'nssmtp' as EmailDeliveryMode but it can't be used."
             } else {
                 set default_send_mode nssmtpd
             }
        }

        switch -- $delivery_mode {
            log {
                set send_mode "log"
                set notice "logging email instead of sending"
            }
            filter {
                set send_mode $default_send_mode
                set allowed_addr [parameter::get \
                                      -package_id $mail_package_id \
                                      -parameter EmailAllow]

                foreach recipient [concat $to_addr $cc_addr $bcc_addr] {

                    # if any of the recipient is not in the allowed list
                    # email message has to be sent to the log instead

                    if {$recipient ni $allowed_addr} {
                        set send_mode "log"
                        set notice "logging email because one of the recipient ($recipient) is not in the EmailAllow list"
                        break
                    }
                }

            }
            redirect {
                set send_mode $default_send_mode

                set redirect_to [parameter::get \
                                 -package_id $mail_package_id \
                                 -parameter EmailRedirectTo]
                if {$redirect_to eq ""} {
                    ns_log warning "acs-mail-lite: redirect mode activated but no value for\
                       EmailRedirectTo provided"
                    set send_mode ignore
                } else {
                    set to_addr $redirect_to
                    set to_addr_header $redirect_to

                    # Since we have to redirect to a list of addresses
                    # we need to remove the CC and BCC
                    set cc_addr ""
                    set bcc_addr ""
                }
            }
            default {
                set send_mode $default_send_mode
            }
        }

        # Prepare the headers list of recipients
        set headers_list [list [list From $from_addr] \
                              [list Reply-To $reply_to] \
                              [list To [join $to_addr_header ","]]]

        if { $cc_addr ne "" } {
            lappend headers_list [list CC [join $cc_addr ","]]
        }

        if { $bcc_addr ne ""} {

            # BCC implementation in tcllib 1.8 to 1.11 is awkward. It
            # sends the blind copy as an attachment, changes the From
            # header replacing it with the originator, etc. So we use
            # DCC instead which behaves as one would expect Bcc to
            # behave.

            lappend headers_list [list DCC [join $bcc_addr ","]]
        }

        set errorMsg ""
        set status ok
        
        ns_log notice "sending mail to $to_addr with send_mode '$send_mode'"
        if {$send_mode eq "nssmtpd"} {

            foreach header $headers_list {
                mime::setheader $tokens [lindex $header 0] [lindex $header 1]
            }
            set fullMailMessage [mime::buildmessage $tokens]

            #
            # Call "ns_smtpd send" from the NaviServer nssmtpd module.
            # When the last two arguments are not provided, the
            # command uses host and port from the configuration
            # section of the nssmtpd module.
            #
            try {
                ns_smtpd send $originator $to_addr fullMailMessage \
                    [dict get $deliveryDict SMTPHost] \
                    [dict get $deliveryDict SMTPPort]
            } on error {errorMsg} {
                set status error
            }

        } elseif$send_mode eq "log" } {

            # Add recipients to headers
            foreach header $headers_list {
                mime::setheader $tokens [lindex $header 0] [lindex $header 1]
            }

            # Retrieve the email message as a string
            set packaged [mime::buildmessage $tokens]

            # Send the email message to the log
            ns_log Notice "acs-mail-lite::send: $notice\n\n**********\n\
                Envelope sender: $originator\n\n$packaged\n**********"

        } elseif {$send_mode eq "smtp"} {

            ad_try {
                acs_mail_lite::smtp -multi_token $tokens \
                    -headers $headers_list \
                    -originator $originator \
                    -delivery_dict $deliveryDict
            } on error {errorMsg} {
                set status error
            }

        } else {
            #
            # Ignoring sending message
            #
            ns_log warning "acs-mail-lite::send: ignore sending message to $to_addr"
        }

        #
        # Close all mime tokens
        #
        mime::finalize $tokens -subordinates all


        if { !$no_callback_p } {
            callback acs_mail_lite::send \
                -package_id $package_id \
                -message_id $message_id \
                -from_addr $from_addr \
                -to_addr $to_addr \
                -body $body \
                -mime_type $mime_type \
                -subject $subject \
                -cc_addr $cc_addr \
                -bcc_addr $bcc_addr \
                -file_ids $file_ids \
                -filesystem_files $filesystem_files \
                -delete_filesystem_files_p $delete_filesystem_files_p \
                -object_id $object_id \
                -status $status \
                -errorMsg $errorMsg
        }

        # Attachment files can now be deleted, if so required.
        # I leave this as the last thing to do, because callbacks
        # could need to look at files for their own purposes.
        if {[string is true $delete_filesystem_files_p]} {
            foreach f $filesystem_files {
            file delete -- $f
            }
        }
        if {$status ne "ok"} {
            error $errorMsg
        }
    }

    #---------------------------------------
    d_proc -deprecated -private message_interpolate {
        {-values:required}
        {-text:required}
    } {
        Interpolates a set of values into a string. This is directly
        copied from the bulk mail package.

        DEPRECATED: duplicated code from bulk-mail that could be replaced by
                    "string map"
        @see "string map"

        @param values a list of key, value pairs, each one consisting of a
        target string and the value it is to be replaced with.
        @param text the string that is to be interpolated

        @return the interpolated string
    } {
        foreach pair $values {
            regsub -all -- [lindex $pair 0] $text [lindex $pair 1] text
        }
        return $text
    }

    #---------------------------------------

    d_proc -public -deprecated ::ns_sendmail {
        to
        from
        subject
        body
        {extraheaders {}}
        {bcc {}}
    } {

        Replacement for ns_sendmail for backward compatibility.

    } {

        ns_log warning "ns_sendmail is deprecated. Use acs_mail_lite::send instead."

        set extraheaders_list [list]

        if { $extraheaders ne "" } {
            foreach {key value} [ns_set array $extraheaders] {
                lappend extraheaders_list [list $key $value]
            }
        }

        acs_mail_lite::send \
            -to_addr [split $to ","] \
            -from_addr $from \
            -subject $subject \
            -body $body \
            -bcc_addr [split $bcc ","] \
            -extraheaders $extraheaders_list
    }

    ad_proc -private address_domain {} {
        @return domain address to which bounces are directed to.
        If empty, uses domain from FixedSenderEmail parameter,
        otherwise the hostname in config.tcl is used.
    } {
        set domain [parameter::get_from_package_key \
                        -package_key "acs-mail-lite" \
                        -parameter "BounceDomain"]
        if { $domain eq "" } {
            # Assume a FixedSenderEmail domain, if it exists.
            set email [parameter::get_from_package_key \
                           -package_key "acs-mail-lite" \
                           -parameter "FixedSenderEmail"]
            if { $email ne "" } {
                set domain [string range $email [string last "@" $email]+1 end]
            } else {
                #
                # If there is no domain configured, use the configured
                # hostname as domain name
                #
                foreach driver [lmap d [ns_driver info] {dict get $d module}] {
                    set section [ns_driversection -driver $driver]
                    set configured_hostname [ns_config $section hostname]
                    if {$configured_hostname ne ""} {
                        set domain $configured_hostname
                        break
                    }
                }
            }
        }
        return $domain
    }

    ad_proc configured_p {} {

        Determine, whether the outgoing SMTPHost is configured and
        reachable.

    } {
        #
        # We have currently no good way to check, whether the outgoing
        # email server is fully configured and accepts our
        # emails. Here we check only, whether we can connect to the
        # configured server.
        #
        set success 0
        set params [get_delivery_parameters]

        try {
            lassign [ns_sockopen -timeout 100ms [dict get $params SMTPHost] [dict get $params SMTPPort]] rid wid
            set readCheck [ns_sockcheck $rid]
            set writeCheck [ns_sockcheck $wid]
            set success [expr {$readCheck && $writeCheck}]
        } on error {errorMsg} {
        } finally {
            catch {close $rid}
            catch {close $wid}
        }

        return $success
    }
}

# Local variables:
#    mode: tcl
#    tcl-indent-level: 4
#    indent-tabs-mode: nil
# End: