• Publicity: Public Only All

maildir-inbound-procs.tcl

Provides API for importing email via postfix maildir

Location:
packages/acs-mail-lite/tcl/maildir-inbound-procs.tcl
Created:
12 Oct 2017
CVS Identification:
$Id: maildir-inbound-procs.tcl,v 1.8.2.1 2019/11/16 16:54:06 gustafn Exp $

Procedures in this file

Detailed information

[ hide source ] | [ make this the default ]

Content File Source

ad_library {

    Provides API for importing email via postfix maildir

    @creation-date 12 Oct 2017
    @cvs-id $Id: maildir-inbound-procs.tcl,v 1.8.2.1 2019/11/16 16:54:06 gustafn Exp $

}

namespace eval acs_mail_lite {}

d_proc -private acs_mail_lite::maildir_check_incoming {
} {
    Checks for new, actionable incoming email via Postfix MailDir standards.
    Email is actionable if it is identified by acs_mail_lite::email_type.

    When actionable, email is buffered in table acs_mail_lite_from_external
    and callbacks are triggered.

    @see acs_mail_lite::email_type

} {
    set error_p 0
    set mail_dir_fullpath [acs_mail_lite::mail_dir]
    if { $mail_dir_fullpath ne "" } {


        set newdir [file join $mail_dir_fullpath new "*"]
        set curdir [file join $mail_dir_fullpath cur "."]

        set messages_list [glob -nocomplain $newdir]

        # only one of acs_mail_lite::maildir_check_incoming process at a time.
        set cycle_start_cs [clock seconds]
        nsv_lappend acs_mail_lite sj_actives_list $cycle_start_cs
        set sj_actives_list [nsv_get acs_mail_lite sj_actives_list]
        ns_log Notice "acs_mail_lite::maildir_check_incoming.37. start \
 sj_actives_list '${sj_actives_list}'"

        set active_cs [lindex $sj_actives_list end]
        set concurrent_ct [llength $sj_actives_list]
        # pause is in seconds
        set pause_s 10
        set pause_ms [expr { $pause_s * 1000 } ]
        while { $active_cs eq $cycle_start_cs
                && $concurrent_ct > 1
            } {
            set sj_actives_list [nsv_get acs_mail_lite sj_actives_list]
            set active_cs [lindex $sj_actives_list end]
            set concurrent_ct [llength $sj_actives_list]
            ns_log Notice "acs_mail_lite::maildir_check_incoming.1198. \
 pausing ${pause_s} seconds for prior invoked processes to stop. \
 sj_actives_list '${sj_actives_list}'"
            after $pause_ms
        }

        if { $active_cs eq $cycle_start_cs } {

            set aml_package_id [apm_package_id_from_key "acs-mail-lite"]
            set filter_proc [parameter::get -parameter "IncomingFilterProcName" \
                                 -package_id $aml_package_id]
            #
            # Iterate through emails
            #
            foreach msg $messages_list {
                set error_p [acs_mail_lite::maildir_email_parse \
                                 -headers_arr_name hdrs_arr \
                                 -parts_arr_name parts_arr \
                                 -message_fpn $msg]
                if { $error_p } {
                    ns_log Notice "acs_mail_lite::maildir_check_incoming \
 could not process message file '${msg}'. Messaged moved to MailDir/cur/."
                    # Move the message into MailDir/cur for other mail reader
                    file copy -- $msg $curdir
                    file delete -- $msg

                } else {
                    # process email

                    set uid $hdrs_arr(uid)
                    set uidvalidity [file mtime $mail_dir_fullpath]
                    set processed_p [acs_mail_lite::inbound_cache_hit_p \
                                         $uid \
                                         $uidvalidity \
                                         $mail_dir_fullpath ]

                    if { !$processed_p } {

                        set type [acs_mail_lite::email_type \
                                      -header_arr_name hdrs_arr ]

                        set headers_list [array names hdrs_arr]
                        # Create some standardized header indexes aml_*
                        # with corresponding values
                        set size_idx [lsearch -nocase -exact \
                                          $headers_list size]
                        set sizen [lindex $headers_list $size_idx]
                        if { $sizen ne "" } {
                            set hdrs_arr(aml_size_chars) $hdrs_arr(${sizen})
                        } else {
                            set hdrs_arr(aml_size_chars) ""
                        }

                        if { [info exists hdrs_arr(received_cs)] } {
                            set hdrs_arr(aml_received_cs) $hdrs_arr(received_cs)
                        } else {
                            set hdrs_arr(aml_received_cs) ""
                        }

                        set su_idx [lsearch -nocase -exact \
                                        $headers_list subject]
                        if { $su_idx > -1 } {
                            set sun [lindex $headers_list $su_idx]
                            set hdrs_arr(aml_subject) [ns_quotehtml $hdrs_arr(${sun})]
                        } else {
                            set hdrs_arr(aml_subject) ""
                        }

                        set to_idx [lsearch -nocase -exact \
                                        $headers_list to]
                        if { ${to_idx} > -1 } {
                            set ton [lindex $headers_list $to_idx]
                            set hdrs_arr(aml_to) [ns_quotehtml $hdrs_arr(${ton}) ]
                        } else {
                            set hdrs_arr(aml_to) ""
                        }

                        acs_mail_lite::inbound_email_context \
                            -header_array_name hdrs_arr \
                            -header_name_list $headers_list

                        acs_mail_lite::inbound_prioritize \
                            -header_array_name hdrs_arr

                        if { [string match {[a-z]*_[a-z]*} $filter_proc] } {
                            set hdrs_arr(aml_package_ids_list) [ad_safe_eval ${filter_proc}]
                        }

                        set id [acs_mail_lite::inbound_queue_insert \
                                    -parts_arr_name parts_arr \
                                    -headers_arr_name hdrs_arr \
                                    -error_p $error_p ]
                        ns_log Notice "acs_mail_lite::maildir_check_incoming \
 inserted to queue aml_email_id '${id}'"
                    }

                    # Move the message into MailDir/cur for other mail handling
                    file copy -- $msg $curdir
                    file delete -- $msg
                }
            }
        }
        # remove active_cs from sj_actives_list
        set sj_idx [lsearch -integer -exact $sj_actives_list $cycle_start_cs]
        # We call nsv_get within nsv_set to reduce chances of dropping
        # a new list entry.
        nsv_set acs_mail_lite sj_actives_list \
            [lreplace [nsv_get acs_mail_lite sj_actives_list] $sj_idx $sj_idx]
        ns_log Notice "acs_mail_lite::maildir_check_incoming.199. stop \
 sj_actives_list '${sj_actives_list}'"
        ns_log Dev "acs_mail_lite::maildir_check_incoming.200. nsv_get \
 acs_mail_lite sj_actives_list '[nsv_get acs_mail_lite sj_actives_list]'"
    }
    # end if !$error

    return 1
}


d_proc -private acs_mail_lite::maildir_email_parse {
    -headers_arr_name
    -parts_arr_name
    {-message_fpn ""}
    {-part_id ""}
    {-section_ref ""}
    {-error_p "0"}
} {
    Parse an email from a Postfix maildir into array array_name
    for adding to queue via acs_mail_lite::inbound_queue_insert
    <br><br>
    Parsed data is set in headers and parts arrays in calling environment.
    @param message_fpn is absolute file path and name of one message
} {
    # Put email in a format usable for
    # acs_mail_lite::inbound_queue_insert to insert into queue

    # We have to generate the references for MailDir..

    # <br><pre>
    # Most basic example of part reference:
    # ref    # part
    # 1    #   message text only

    # More complex example. Order is not enforced, only hierarchy.
    # ref    # part
    # 1    #   multipart message
    # 1.1    # part 1 of ref 1
    # 1.2    # part 2 of ref 1
    # 4    #   part 1 of ref 4
    # 3.1    # part 1 of ref 3
    # 3.2    # part 2 of ref 3
    # 3.5    # part 5 of ref 3
    # 3.3    # part 3 of ref 3
    # 3.4    # part 4 of ref 3
    # 2    #   part 1 of ref 2

    # Due to the hierarchical nature of email, this proc is recursive.
    # To see examples of struct list to build, see www/doc/imap-notes.txt
    # and www/doc/maildir-test.tcl
    # reference mime procs:
    # https://www.tcl.tk/community/tcl2004/Tcl2003papers/kupries-doctools/tcllib.doc/mime/mime.html

    upvar 1 $headers_arr_name h_arr
    upvar 1 $parts_arr_name p_arr
    upvar 1 __max_txt_bytes __max_txt_bytes
    set has_parts_p 0
    set section_n_v_list [list ]
    # RFC 822 date time format regexp expression
    set re822 {[^a-z]([a-z][a-z][a-z][ ,]+[0-9]+ [a-z][a-z][a-z][ ]+[0-9][0-9][0-9][0-9][ ]+[0-9][0-9][:][0-9][0-9][:][0-9][0-9][ ]+[\+\-][0-9]+)[^0-9]}

    if { ![info exists __max_txt_bytes] } {
        set sp_list [acs_mail_lite::sched_parameters]
        set __max_txt_bytes [dict get $sp_list max_blob_chars]
    }
    if { $message_fpn ne "" } {
        if {[catch {set m_id [mime::initialize -file ${message_fpn}] } errmsg] } {
            ns_log Error "maildir_email_parse.71 could not parse \
 message file '${message_fpn}' error: '${errmsg}'"
            set error_p 1
        } else {
            # For acs_mail_lite::inbond_cache_hit_p,
            # make a uid if there is not one.
            set uid_ref ""
            # Do not use email file's tail,
            # because tail is unique to system not email.
            # See http://cr.yp.to/proto/maildir.html

            # A header returns multiple values in a list
            # if header name is repeated in email.
            set h_list [mime::getheader $m_id]
            # headers_list
            set headers_list [list ]
            foreach {h v} $h_list {
                switch -nocase -- $h {
                    uid {
                        if { $h ne "uid" } {
                            lappend struct_list "uid" $v
                        }
                        set uid_ref "uid"
                        set uid_val $v
                    }
                    message-id -
                    msg-id {
                        if { $uid_ref ne "uid"} {
                            if { $uid_ref ne "message-id" } {
                                # message-id is not required
                                # msg-id is an alternate
                                # Fallback to most standard uid
                                set uid_ref [string tolower $h]
                                set uid_val $v
                            }
                        }
                    }
                    received {
                        if { [llength $v ] > 1 } {
                            set v0 [lindex $v 0]
                        } else {
                            set v0 $v
                        }
                        if { [regexp -nocase -- $re822 $v0 match r_ts] } {
                            set age_s [mime::parsedatetime $r_ts rclock]
                            set dt_cs [expr { [clock seconds] - $age_s } ]
                            lappend headers_list "aml_datetime_cs" $dt_cs
                        }
                    }
                    default {
                        # do nothing
                    }
                }
                lappend headers_list $h $v
            }
            lappend headers_list "aml_received_cs" [file mtime ${message_fpn}]
            lappend headers_list "uid" $uid_val

            # Append property_list to headers_list
            set prop_list [mime::getproperty $m_id]
            #set prop_names_list /mime::getproperty $m_id -names/
            foreach {n v} $prop_list {
                switch -nocase -exact -- $n {
                    params {
                        # extract name as header filename
                        foreach {m w} $v {
                            if { [string match -nocase "*name" $m] } {
                                regsub -all -nocase -- {[^0-9a-zA-Z-.,\_]} $w {_} w
                                if { $w eq "" } {
                                    set w "untitled"
                                }
                                set filename $w
                                lappend headers_list "filename" $w
                            } else {
                                lappend headers_list $m $w
                            }
                        }
                    }
                    default {
                        lappend headers_list $n $v
                    }
                }
            }
        }
        if { $section_ref eq "" } {
            set section_ref 1
        }
        set subref_ct 0
        set type ""

        # Assume headers and names are unordered
        foreach {n v} $headers_list {
            if { [string match -nocase {parts} $n] } {
                set has_parts_p 1
                foreach part_id $v {
                    incr subref_ct
                    set subref $section_ref
                    append subref "." $subref_ct
                    acs_mail_lite::maildir_email_parse \
                        -headers_arr_name h_arr \
                        -parts_arr_name p_arr \
                        -part_id $part_id \
                        -section_ref $subref
                }
            } else {
                switch -exact -nocase -- $n {
                    size {
                        set bytes $v
                    }
                    # content-type
                    content {
                        set type $v
                    }
                    default {
                        # do nothing
                    }
                }
                if { $section_ref eq "1" } {
                    set h_arr(${n}${v}
                } else {
                    lappend section_n_v_list ${n} ${v}
                }
            }
        }

        set section_id [acs_mail_lite::section_id_of $section_ref]
        ns_log Dev "acs_mail_lite::maildir_email_parse.746 \
message_fpn '${message_fpn}' section_ref '${section_ref}' section_id '${section_id}'"

        # Add content of an email part
        set p_arr(${section_id},nv_list) $section_n_v_list
        set p_arr(${section_id},c_type) $type
        lappend p_arr(section_id_list) ${section_id}

        if { [info exists bytes]
             && $bytes > $__max_txt_bytes
             && ![info exists filename]
         } {
            set filename "blob.txt"
        }

        if { [info exists filename] } {
            set filename2 [clock microseconds]
            append filename2 "-" $filename
            set filepathname [file join [acs_root_dir] \
                                  acs-mail-lite \
                                  $filename2 ]
            set p_arr(${section_id},filename) $filename
            set p_arr(${section_id},c_filepathname) $filepathname
            if { $filename eq "blob.txt" } {
                ns_log Dev "acs_mail_lite::maildir_email_parse.775 \
 m_id '${m_id}' '${section_ref}' \
 -file '${filepathname}'"
                set txtfileId [open $filepathname "w"]
                puts -nonewline $txtfileId [mime::getbody $m_id]
                close $txtfileId
            } else {
                ns_log Dev "acs_mail_lite::maildir_email_parse.780 \
 mime::getbody '${m_id}' '${section_ref}' \
 -file '${filepathname}' -decode"
                set binfileId [open $filepathname "w"]
                chan configure $binfileId -translation binary
                puts -nonewline $binfileId [mime::getbody $m_id -decode ]
                close $binfileId
            }
        } elseif$section_ref ne "" } {
            # text content
            set p_arr(${section_id},content) [mime::buildmessage $m_id]
            ns_log Dev "acs_mail_lite::maildir_email_parse.792 \
 text m_id '${m_id}' '${section_ref}': \
 $p_arr(${section_id},content)'"

        } else {
            set p_arr(${section_id},content) ""
            # The content for this case
            # has been verified to be redundant.
            # It is mostly the last section/part of message.
            #
            # If diagnostics urge examining these cases,
            # Set debug_p 1 to allow the following code
            # to compress a message to recognizable parts without
            # flooding the log.
            set debug_p 0
            if { $debug_p } {
                set msg_txt [mime::buildmessage $m_id]
                # 72 character wide lines * x lines
                set msg_start_max [expr { 72 * 20 } ]
                set msg_txtb [string range $msg_txt 0 $msg_start_max]
                if { [string length $msg_txt] > $msg_start_max + 400 } {
                    set msg_txte [string range $msg_txt end-$msg_start_max end]
                } elseif { [string length $msg_txt] > $msg_start_max + 144 } {
                    set msg_txte [string range $msg_txt end-144 end]
                } else {
                    set msg_txte ""
                }
                ns_log Dev "acs_mail_lite::maildir_email_parse.818 IGNORED \
 text '${message_fpn}' '${section_ref}' \n \
 msg_txte '${msg_txte}'"
            } else {
                ns_log Dev "acs_mail_lite::maildir_email_parse.822 ignored \
 text '${message_fpn}' '${section_ref}'"
            }
        }
    }
    return $error_p
}


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