Source file splitmail.icn
#<p>
# SplitMail -- split messages from system mailbox by matching substrings in
#              Subject: and/or From: lines.  Can handle arbitrary sized
#              system mailboxes.
#</p>
#<p>
# Options:
#<ul>
#  <li><tt>--help</tt> -- display a usage message and quit
#  <li><tt>--name=USER</tt> -- read from this user's mailbox
#  <li><tt>--file=FILENAME</tt> -- read from this file
#  <li><tt>--subject=SUBSTRING</tt> (<i>may be repeated</i>) <tt>Subject:</tt>
#                              substring to search for
#  <li><tt>--from=SUBSTRING</tt> (<i>may be repeated</i>) <tt>From:</tt>
#                              substring to search for
#  <li><tt>--withfile=FILENAME</tt> (<i>optional</i>) file to hold messages
#                              matching any of the search criteria
#  <li><tt>--withoutfile=FILENAME</tt> (<i>optional</i>) file to hold messages
#                              matching none of the search criteria
#  <li><tt>--verbose</tt> (<i>optional</i>) show how each message is checked,
#                         <tt>+++</tt> indicates a match, <tt>---</tt> a
#                         mismatch
#  <li><tt>--append</tt> (<i>optional</i>) append messages to output files
#                         instead of overwriting on each run
#  <li><tt>--dryrun</tt> (<i>optional</i>) turn on verbose mode but don't
#                         create output files
#</ul>
#<p>
#Exactly one of <tt>--name</tt> or <tt>--file</tt> is <i>required</i>.
#When <tt>--file</tt> is used, the default names for the output files start with
#<tt>basename</tt> of the named file.
#</p>
#<p>
#You must have at least one instance of <tt>--subject</tt> or <tt>--from</tt>.
#When the option uses <tt>--subject=</tt> or <tt>--from=</tt> then the
#match is case sensitive.  Replacing <tt>=</tt> with <tt>~</tt> as in
#<tt>--subject~happy</tt> results in a case insensitive match.
#</p>
#<p>
#<tt>FILENAME</tt> may be <tt>/dev/null</tt> to suppress output,
#and defaults to <i>USER</i><tt>.with</tt> for <tt>--withfile</tt> and
#<i>USER</i><tt>.without</tt> for <tt>--withoutfile</tt>.
#</p>

import util

record message(from,subject,date,text)  # Internal form for mail message

global dryrun,    # If non-null, don't create output files
       verbose,   # If non-null, show how messages are categorized
       append,    # If non-null, append to output files instead of overwriting
       sList,     # List of Subject: substrings to look for
       fList,     # List of From: substrings to look for
       sCiList,   # List of case-insensitive Subject: substrings to look for
       fCiList    # List of case-insensitive From: substrings to look for

#<p>
#  Read a user's system mailbox and split messages into two separate files
#   based on substrings found in From: and Subject: lines.
#</p>
procedure main(args)
    if "--help" == !args then stop(helpMesg())
    if whoami := zapPrefix(!args, "--name=") then {
        mailFileName := "/var/spool/mail/"||whoami
        fileName := whoami
        }
    if inFile := zapPrefix(!args, "--file=") then {
        mailFileName := inFile
        fileName := basename(mailFileName)
        }
    if /whoami & /inFile then stop(helpMesg())
    if \whoami & \inFile then stop(helpMesg())

    withFile    := zapPrefix(!args, "--withfile=")    | (fileName||".with")
    withoutFile := zapPrefix(!args, "--withoutfile=") | (fileName||".without")

    every sList := put([], zapPrefix(!args, "--subject="))
    every fList := put([], zapPrefix(!args, "--from="))
    every sCiList := put([], map(zapPrefix(!args, "--subject~")))
    every fCiList := put([], map(zapPrefix(!args, "--from~")))
    if /sList & /fList & /sCiList & / fCiList then stop(helpMesg())

    verbose     := ("--verbose" == !args)
    append      := ("--append" == !args)
    dryrun      := ("--dryrun" == !args)

    oMode := if \append then "wa" else "w"
    if \dryrun then verbose := "yes"
    if \verbose then {
        if \append then write(&errout, "append mode")
        if \sList then showList(&errout, "Subject phrases", sList)
        if \sCiList then showList(&errout, "Case insensitive subject phrases",
                                           sCiList)
        if \fList then showList(&errout, "From phrases", fList)
        if \fCiList then showList(&errout, "Case insensitive from phrases",
                                           fCiList)
        write(&errout)
        }

    if /dryrun then f1 := open(withFile, oMode)
    if /dryrun then f2 := open(withoutFile, oMode)

    while routeMessage(readMessages(mailFileName), f1, f2)

    if /dryrun then every close(f1|f2)
end

#<p>
#  Display the usage information to standard error.
#</p>
procedure helpMesg()
    write(&errout, "Usage: SplitMail (--name=USER | --file=FILENAME)")
    write(&errout, "                 --subject=SUBJECT_SUBSTRING")
    write(&errout, "                 --from=FROM_SUBSTRING")
    write(&errout, "                 [--withfile=FILENAME]")
    write(&errout, "                 [--withoutfile=FILENAME]")
    write(&errout, "                 [--append] [--verbose] [--dryrun]")
    write(&errout)
    write(&errout, "splits the USER's system mailbox into two sets of")
    write(&errout, "messages: USER.with holds all messages whose subject")
    write(&errout, "and from lines contains any of the SUBJECT_SUBSTRINGs")
    write(&errout, "or any of the FROM_SUBSTRINGs; USER.without holds all")
    write(&errout, "remaining mail messages.  The original system mailbox")
    write(&errout, "is untouched.  Both --subject and --from arguments may")
    write(&errout, "be repeated 0 or more times, but at least one should")
    write(&errout, "be used.")
    write(&errout)
    write(&errout, "When the option uses --subject= or --from= then the")
    write(&errout, "match is case sensitive.  Replacing = with ~ as in")
    write(&errout, "--subject~happy results in a case insensitive match.")
    write(&errout)
    write(&errout, "The --file option may be used in place of --user to")
    write(&errout, "explicitly name an input file.  When --file is used,")
    write(&errout, "output file names start with the basename of the filename")
    write(&errout, "unless overridden with --withfile and/or --withoutfile")
    write(&errout)
    write(&errout, "The --withfile and --withoutfile options may be used to")
    write(&errout, "override the default output file names.  Unix/Linux users")
    write(&errout, "may find --withoutfile=/dev/null appealing.")
    write(&errout)
    write(&errout, "With --append, appends to both files instead of overwriting them.")
    write(&errout, "With --verbose, shows simple header for messages that")
    write(&errout, "match the search criteria (prefixed by '+++') and don't")
    write(&errout, "match the search criteria (prefixed by '---').")
    write(&errout)
    write(&errout, "The --dryrun option adds --verbose and disables the actual")
    write(&errout, "splitting of the messages into files.")
    return
end

#<p>
# Generate the mail messages from the users system mailbox.
# Produces each mail messages in internal form.
#</p>
procedure readMessages(mailFileName)
    mailFile := PushBack(open(mailFileName)) | fail
    while line := mailFile.read() do {
        if match("From ",line) then {
            suspend getMessage(line, mailFile) 
            }
        }
end

#<p>
#  Read a single mail message from a mail file.
#</p>
procedure getMessage(line, mailFile)
    static WS := ' \t'
    local date, from, subject
    lines := [line]

    line ?  date := (word(),word(),tab(0))
    while line := mailFile.read() do {
        line ? {
            if      ="From: "      then /from := tab(0)
            else if =("Subject: " |
                      "subject: ") then /subject := tab(0)
            else if match("From ") then {
                break mailFile.pushback(line)
                }
            }
        put(lines, line)
        }

    return message((\from)|"Unknown!",(\subject)|"No subject!",date, lines)
end

#<p>
#  Given a single mail message, routes it to the appropriate
#  output file based on the selection criteria.
#</p>
procedure routeMessage(message, file1, file2)
    msCi := map(message.subject)
    mfCi := map(message.from)
    if find(!\sList,   message.subject) |
       find(!\sCiList, msCi)            |
       find(!\fList,   message.from)    |
       find(!\fCiList, mfCi)            then {
        if \verbose then showHeader(&errout, "+++",message)
        if /dryrun then every write(file1, !message.text)
        }
    else {
        if \verbose then showHeader(&errout, "---",message)
        if /dryrun then every write(file2, !message.text)
        }
        
end

#<p>
#  String scanning procedure that generates the next word
#</p>
procedure word()
    static WS := ' \t'
    suspend (tab(many(WS)),tab(many(~WS)))
end

#<p>
#  Produce the filename at the end of a Unix pathname.
#</p>
procedure basename(s)
    reverse(s) ? {
        return (reverse(tab(upto('/'))) | s)
        }
end

#<p>
#  Display a list of values on one line, preceded by some prefix.
#  <i>Used in verbose mode.</i>
#</p>
procedure showList(f, prefix, aList)
    if \aList then {
        writes(f, prefix,":")
        every writes(f, " ",!aList)
        write(f)
        }
end

#<p>
#   Show a summary of a mail message's header.
#   <i>Used in verbose mode.</i>
#</p>
procedure showHeader(f, prefix, h)
    write(f, prefix, left(mapFrom(h.from),20),": ",left(h.subject,57),
          " (",*(h.text),")")
end

#<p>
#   Produce the 'from' information from a From: line.
#   <i>Used in verbose mode.</i>
#</p>
procedure mapFrom(s)
    local ns := ""
    \s ? {
        ns := if      ="\"" then tab(upto('"'))
              else if ="<"  then tab(upto('>'))
              else               tab(upto('<(')|0)
        }
    return trim(ns," \t")
end

This page produced by UniDoc on 2021/04/15 @ 23:59:43.