2023-06-18 02:37:25 +00:00
|
|
|
# vim: set et nosta sw=4 ts=4 :
|
|
|
|
|
#
|
2023-06-18 17:04:02 +00:00
|
|
|
# A class that represents an individual Maildir, and a Nessage class to nanage
|
|
|
|
|
# files underneath them.
|
2023-06-18 02:37:25 +00:00
|
|
|
#
|
|
|
|
|
|
|
|
|
|
#############################################################
|
|
|
|
|
# I M P O R T S
|
|
|
|
|
#############################################################
|
|
|
|
|
|
|
|
|
|
import
|
|
|
|
|
std/os,
|
2023-06-18 17:04:02 +00:00
|
|
|
std/osproc,
|
|
|
|
|
std/posix,
|
2023-06-21 03:02:52 +00:00
|
|
|
std/re,
|
2023-06-18 02:37:25 +00:00
|
|
|
std/streams,
|
|
|
|
|
std/strformat,
|
2023-06-21 03:02:52 +00:00
|
|
|
std/strutils,
|
|
|
|
|
std/tables,
|
2023-06-18 02:37:25 +00:00
|
|
|
std/times
|
|
|
|
|
|
|
|
|
|
import
|
2023-06-21 03:02:52 +00:00
|
|
|
config,
|
2023-06-18 02:37:25 +00:00
|
|
|
util
|
|
|
|
|
|
|
|
|
|
|
2023-06-18 17:04:02 +00:00
|
|
|
#############################################################
|
|
|
|
|
# C O N S T A N T S
|
|
|
|
|
#############################################################
|
|
|
|
|
|
|
|
|
|
const
|
|
|
|
|
OWNERDIRPERMS = { fpUserExec, fpUserWrite, fpUserRead }
|
|
|
|
|
OWNERFILEPERMS = { fpUserWrite, fpUserRead }
|
2023-06-21 03:02:52 +00:00
|
|
|
FILTERPROCOPTS = { poUsePath }
|
|
|
|
|
# FILTERPROCOPTS = { poUsePath, poEvalCommand }
|
2023-06-18 17:04:02 +00:00
|
|
|
BUFSIZE = 8192 # reading and writing buffer size
|
|
|
|
|
|
|
|
|
|
|
2023-06-18 02:37:25 +00:00
|
|
|
#############################################################
|
|
|
|
|
# T Y P E S
|
|
|
|
|
#############################################################
|
|
|
|
|
|
|
|
|
|
# A Maildir object.
|
|
|
|
|
#
|
|
|
|
|
type Maildir* = ref object
|
|
|
|
|
path*: string # Absolute path to the encapsualting dir
|
|
|
|
|
cur: string
|
|
|
|
|
new: string
|
|
|
|
|
tmp: string
|
|
|
|
|
|
|
|
|
|
# An email message, under a specific Maildir.
|
|
|
|
|
#
|
|
|
|
|
type Message* = ref object
|
2023-06-18 17:04:02 +00:00
|
|
|
basename: string
|
|
|
|
|
dir: Maildir
|
2023-06-21 03:02:52 +00:00
|
|
|
headers*: Table[ string, seq[string] ]
|
2023-06-18 17:04:02 +00:00
|
|
|
path: string
|
|
|
|
|
stream: FileStream
|
2023-06-18 02:37:25 +00:00
|
|
|
|
|
|
|
|
|
2023-06-29 12:58:42 +00:00
|
|
|
var
|
|
|
|
|
msgcount = 0 # Count messages generated during a run.
|
|
|
|
|
msgId = "" # The parsed Message-ID
|
2023-06-20 11:26:41 +00:00
|
|
|
|
|
|
|
|
|
2023-06-18 02:37:25 +00:00
|
|
|
#############################################################
|
|
|
|
|
# M E T H O D S
|
|
|
|
|
#############################################################
|
|
|
|
|
|
2023-06-20 11:26:41 +00:00
|
|
|
#------------------------------------------------------------
|
|
|
|
|
# Maildir
|
|
|
|
|
#------------------------------------------------------------
|
|
|
|
|
|
2023-06-18 02:37:25 +00:00
|
|
|
proc newMaildir*( path: string ): Maildir =
|
|
|
|
|
## Create and return a new Maildir object, making it on-disk if necessary.
|
|
|
|
|
result = new Maildir
|
|
|
|
|
result.path = path
|
2023-06-20 11:26:41 +00:00
|
|
|
result.cur = joinPath( path, "cur" )
|
|
|
|
|
result.new = joinPath( path, "new" )
|
|
|
|
|
result.tmp = joinPath( path, "tmp" )
|
2023-06-18 02:37:25 +00:00
|
|
|
|
|
|
|
|
if not dirExists( path ):
|
2023-06-23 16:27:37 +00:00
|
|
|
"Creating new maildir at $#".debug( path )
|
2023-06-18 02:37:25 +00:00
|
|
|
try:
|
|
|
|
|
for p in [ result.path, result.cur, result.new, result.tmp ]:
|
|
|
|
|
p.createDir
|
2023-06-18 17:04:02 +00:00
|
|
|
p.setFilePermissions( OWNERDIRPERMS )
|
2023-06-18 02:37:25 +00:00
|
|
|
|
|
|
|
|
except CatchableError as err:
|
|
|
|
|
deferral "Unable to create Maildir: ({err.msg}), deferring delivery.".fmt
|
|
|
|
|
|
|
|
|
|
|
2023-06-18 17:04:02 +00:00
|
|
|
proc subDir*( dir: Maildir, path: string ): Maildir =
|
|
|
|
|
## Creates a new Maildir relative to an existing one.
|
|
|
|
|
result = newMaildir( dir.path & "/" & path )
|
|
|
|
|
|
|
|
|
|
|
2023-06-20 11:26:41 +00:00
|
|
|
#------------------------------------------------------------
|
|
|
|
|
# Message
|
|
|
|
|
#------------------------------------------------------------
|
|
|
|
|
|
2023-06-18 02:37:25 +00:00
|
|
|
proc newMessage*( dir: Maildir ): Message =
|
|
|
|
|
## Create and return a Message - an open FileStream under a specific Maildir
|
|
|
|
|
## (in tmp)
|
|
|
|
|
result = new Message
|
|
|
|
|
|
|
|
|
|
let now = getTime()
|
2023-06-18 17:04:02 +00:00
|
|
|
var hostname = newString(256)
|
|
|
|
|
discard getHostname( cstring(hostname), cint(256) )
|
2023-06-18 02:37:25 +00:00
|
|
|
|
2023-06-20 11:26:41 +00:00
|
|
|
msgcount = msgcount + 1
|
2023-06-18 17:04:02 +00:00
|
|
|
result.dir = dir
|
2023-06-20 11:26:41 +00:00
|
|
|
result.basename = $now.toUnixFloat & '.' & $getCurrentProcessID() & '.' & $msgcount & '.' & $hostname
|
|
|
|
|
result.path = joinPath( result.dir.tmp, result.basename )
|
2023-06-18 17:04:02 +00:00
|
|
|
|
|
|
|
|
try:
|
2023-06-23 16:27:37 +00:00
|
|
|
"Opening new message at:\n $#".debug( result.path )
|
2023-06-18 17:04:02 +00:00
|
|
|
result.stream = openFileStream( result.path, fmWrite )
|
|
|
|
|
result.path.setFilePermissions( OWNERFILEPERMS )
|
|
|
|
|
except CatchableError as err:
|
2023-06-20 11:26:41 +00:00
|
|
|
deferral "Unable to write file {result.path} {err.msg}".fmt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc open*( msg: Message ) =
|
2023-06-21 03:02:52 +00:00
|
|
|
## Open (or re-open) a Message file stream for reading.
|
2023-06-20 11:26:41 +00:00
|
|
|
msg.stream = msg.path.openFileStream
|
2023-06-18 17:04:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
proc save*( msg: Message, dir=msg.dir ) =
|
|
|
|
|
## Move the message from tmp to new. Defaults to its current
|
|
|
|
|
## maildir, but can be provided a different one.
|
2023-06-20 11:26:41 +00:00
|
|
|
msg.stream.close
|
|
|
|
|
let newpath = joinPath( dir.new, msg.basename )
|
2023-06-18 17:04:02 +00:00
|
|
|
msg.path.moveFile( newpath )
|
2023-06-23 16:27:37 +00:00
|
|
|
"Delivered message to:\n $#".debug( newpath )
|
2023-06-18 17:04:02 +00:00
|
|
|
msg.dir = dir
|
|
|
|
|
msg.path = newpath
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proc delete*( msg: Message ) =
|
|
|
|
|
## Remove a message from disk.
|
2023-06-20 11:26:41 +00:00
|
|
|
msg.stream.close
|
2023-06-18 17:04:02 +00:00
|
|
|
msg.path.removeFile
|
2023-06-23 16:27:37 +00:00
|
|
|
"Removed message at:\n $#".debug( msg.path )
|
2023-06-18 17:04:02 +00:00
|
|
|
msg.path = ""
|
|
|
|
|
|
|
|
|
|
|
2023-06-21 03:02:52 +00:00
|
|
|
proc writeStdin*( msg: Message ): Message =
|
|
|
|
|
## Streams stdin to the message file, returning the Message
|
|
|
|
|
## object for chaining.
|
2023-06-20 11:26:41 +00:00
|
|
|
let input = stdin.newFileStream
|
2023-06-21 03:02:52 +00:00
|
|
|
var total = 0
|
|
|
|
|
result = msg
|
2023-06-18 17:04:02 +00:00
|
|
|
|
2023-06-21 03:02:52 +00:00
|
|
|
while not input.atEnd:
|
|
|
|
|
let buf = input.readStr( BUFSIZE )
|
2023-06-18 17:04:02 +00:00
|
|
|
total = total + buf.len
|
|
|
|
|
msg.stream.write( buf )
|
2023-06-20 11:26:41 +00:00
|
|
|
msg.stream.flush
|
|
|
|
|
msg.stream.close
|
2023-06-23 16:27:37 +00:00
|
|
|
"Wrote $# bytes".debug( total )
|
2023-06-18 17:04:02 +00:00
|
|
|
|
|
|
|
|
|
2023-06-21 03:02:52 +00:00
|
|
|
proc filter*( orig_msg: Message, cmd: seq[string] ): Message =
|
2023-06-20 11:26:41 +00:00
|
|
|
## Filter message content through an external program,
|
|
|
|
|
## returning a new Message if successful.
|
2023-06-21 03:02:52 +00:00
|
|
|
result = orig_msg
|
2023-06-20 11:26:41 +00:00
|
|
|
try:
|
|
|
|
|
var buf: string
|
|
|
|
|
|
2023-06-21 03:02:52 +00:00
|
|
|
let process = cmd[0].startProcess(
|
|
|
|
|
args = cmd[1..(cmd.len-1)],
|
|
|
|
|
options = FILTERPROCOPTS
|
|
|
|
|
)
|
2023-06-20 11:26:41 +00:00
|
|
|
|
2023-06-23 16:27:37 +00:00
|
|
|
"Running filter: $#".debug( cmd )
|
2023-06-21 03:02:52 +00:00
|
|
|
# let process = cmd.startProcess( options = FILTERPROCOPTS )
|
2023-06-20 11:26:41 +00:00
|
|
|
|
|
|
|
|
# Read from the original message, write to the filter
|
|
|
|
|
# process in chunks.
|
|
|
|
|
#
|
|
|
|
|
orig_msg.open
|
2023-06-21 03:02:52 +00:00
|
|
|
while not orig_msg.stream.atEnd:
|
2023-06-20 11:26:41 +00:00
|
|
|
buf = orig_msg.stream.readStr( BUFSIZE )
|
|
|
|
|
process.inputStream.write( buf )
|
|
|
|
|
process.inputStream.flush
|
|
|
|
|
|
|
|
|
|
# Read from the filter process until EOF, send to the
|
|
|
|
|
# new message in chunks.
|
2023-06-21 03:02:52 +00:00
|
|
|
#
|
2023-06-20 11:26:41 +00:00
|
|
|
process.inputStream.close
|
|
|
|
|
let new_msg = newMessage( orig_msg.dir )
|
2023-06-21 03:02:52 +00:00
|
|
|
while not process.outputStream.atEnd:
|
2023-06-20 11:26:41 +00:00
|
|
|
buf = process.outputStream.readStr( BUFSIZE )
|
|
|
|
|
new_msg.stream.write( buf )
|
|
|
|
|
new_msg.stream.flush
|
|
|
|
|
|
|
|
|
|
let exitcode = process.waitForExit
|
2023-06-23 16:27:37 +00:00
|
|
|
"Filter exited: $#".debug( exitcode )
|
2023-06-20 11:26:41 +00:00
|
|
|
process.close
|
2023-06-21 03:02:52 +00:00
|
|
|
if exitcode == 0:
|
|
|
|
|
new_msg.stream.close
|
|
|
|
|
orig_msg.delete
|
|
|
|
|
result = new_msg
|
|
|
|
|
else:
|
2023-06-23 16:27:37 +00:00
|
|
|
"Unable to filter message: non-zero exit code".debug
|
2023-06-20 11:26:41 +00:00
|
|
|
|
|
|
|
|
except OSError as err:
|
2023-06-23 16:27:37 +00:00
|
|
|
"Unable to filter message: $#".debug( err.msg )
|
2023-06-20 11:26:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-06-21 03:02:52 +00:00
|
|
|
proc parseHeaders*( msg: Message ) =
|
|
|
|
|
## Walk the RFC2822 headers, placing them into memory.
|
|
|
|
|
## This 'unwraps' multiline headers, and allows for duplicate headers.
|
|
|
|
|
msg.headers = initTable[ string, seq[string] ]()
|
|
|
|
|
msg.open
|
|
|
|
|
|
|
|
|
|
var
|
|
|
|
|
line = ""
|
|
|
|
|
header = ""
|
|
|
|
|
value = ""
|
2023-06-18 02:37:25 +00:00
|
|
|
|
2023-06-21 03:02:52 +00:00
|
|
|
while msg.stream.readLine( line ):
|
|
|
|
|
if line == "": # Stop when headers are done.
|
|
|
|
|
if header != "":
|
|
|
|
|
if msg.headers.hasKey( header ):
|
|
|
|
|
msg.headers[ header ].add( value )
|
|
|
|
|
else:
|
|
|
|
|
msg.headers[ header ] = @[ value ]
|
|
|
|
|
break
|
2023-06-20 11:26:41 +00:00
|
|
|
|
2023-06-21 03:02:52 +00:00
|
|
|
# Fold continuation line
|
|
|
|
|
#
|
2023-06-29 12:58:42 +00:00
|
|
|
let wsp = re"^\s+"
|
|
|
|
|
if line.match( wsp ):
|
|
|
|
|
line = line.replace( wsp )
|
2023-06-21 03:02:52 +00:00
|
|
|
value = value & ' ' & line
|
|
|
|
|
|
|
|
|
|
# Header start
|
|
|
|
|
#
|
|
|
|
|
else:
|
|
|
|
|
var matches: array[ 2, string ]
|
|
|
|
|
if line.match( re"^([\w\-]+):\s*(.*)", matches ):
|
|
|
|
|
if header != "":
|
|
|
|
|
if msg.headers.hasKey( header ):
|
|
|
|
|
msg.headers[ header ].add( value )
|
|
|
|
|
else:
|
|
|
|
|
msg.headers[ header ] = @[ value ]
|
|
|
|
|
( header, value ) = ( matches[0].toLower, matches[1] )
|
|
|
|
|
|
2023-06-29 12:58:42 +00:00
|
|
|
if msgId == "":
|
|
|
|
|
"Parsed message headers.".debug
|
|
|
|
|
if msg.headers.hasKey( "message-id" ):
|
|
|
|
|
msgId = msg.headers[ "message-id" ][0]
|
|
|
|
|
"Message-ID is \"$#\"".debug( msg.headers[ "message-id" ] )
|
2023-06-23 16:27:37 +00:00
|
|
|
|
2023-06-21 03:02:52 +00:00
|
|
|
|
2023-06-21 04:14:54 +00:00
|
|
|
proc evalRules*( msg: var Message, rules: seq[Rule], default: Maildir ): bool =
|
2023-06-21 03:02:52 +00:00
|
|
|
## Evaluate each rule against the Message, returning true
|
|
|
|
|
## if there was a valid match found.
|
|
|
|
|
result = false
|
2023-06-21 04:14:54 +00:00
|
|
|
msg.parseHeaders
|
2023-06-21 03:02:52 +00:00
|
|
|
|
|
|
|
|
for rule in rules:
|
|
|
|
|
var match = false
|
|
|
|
|
|
2023-06-29 12:58:42 +00:00
|
|
|
if rule.match.len > 0: "Evaluating rule...".debug
|
2023-06-21 03:02:52 +00:00
|
|
|
block thisRule:
|
2023-06-29 12:58:42 +00:00
|
|
|
for header, regexp in rule.match:
|
2023-06-21 03:02:52 +00:00
|
|
|
let header_chk = header.toLower
|
2023-06-21 04:14:54 +00:00
|
|
|
var
|
|
|
|
|
hmatch = false
|
|
|
|
|
headerlbl = header_chk
|
|
|
|
|
recipients: seq[ string ]
|
|
|
|
|
values: seq[ string ]
|
|
|
|
|
|
|
|
|
|
# TO checks both To: and Cc: simultaneously.
|
|
|
|
|
#
|
|
|
|
|
if header == "TO":
|
|
|
|
|
headerlbl = "to|cc"
|
|
|
|
|
recipients = msg.headers.getOrDefault( "to", @[] ) &
|
|
|
|
|
msg.headers.getOrDefault( "cc", @[] )
|
|
|
|
|
|
2023-06-23 16:27:37 +00:00
|
|
|
" checking header \"$#\"".debug( headerlbl )
|
2023-06-21 04:14:54 +00:00
|
|
|
if recipients.len > 0:
|
|
|
|
|
values = recipients
|
|
|
|
|
elif msg.headers.hasKey( header_chk ):
|
|
|
|
|
values = msg.headers[ header_chk ]
|
|
|
|
|
else:
|
2023-06-23 16:27:37 +00:00
|
|
|
" nonexistent header, skipping others".debug
|
2023-06-21 04:14:54 +00:00
|
|
|
break thisRule
|
|
|
|
|
|
|
|
|
|
for val in values:
|
|
|
|
|
try:
|
|
|
|
|
hmatch = val.match( regexp.re({reStudy,reIgnoreCase}) )
|
|
|
|
|
if hmatch:
|
2023-06-23 16:27:37 +00:00
|
|
|
" match on \"$#\"".debug( regexp )
|
2023-06-21 04:14:54 +00:00
|
|
|
break # a single multi-header is sufficient
|
|
|
|
|
except RegexError as err:
|
2023-06-23 16:27:37 +00:00
|
|
|
let errmsg = err.msg.replace( "\n", " " )
|
|
|
|
|
" invalid regexp \"$#\" ($#), skipping".debug( regexp, errmsg )
|
2023-06-21 03:02:52 +00:00
|
|
|
break thisRule
|
|
|
|
|
|
2023-06-21 04:14:54 +00:00
|
|
|
# Did any of the (possibly) multi-header values match?
|
|
|
|
|
if hmatch:
|
|
|
|
|
match = true
|
2023-06-21 03:02:52 +00:00
|
|
|
else:
|
2023-06-23 16:27:37 +00:00
|
|
|
" no match for \"$#\", skipping others".debug( regexp )
|
2023-06-21 03:02:52 +00:00
|
|
|
break thisRule
|
|
|
|
|
|
|
|
|
|
result = match
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if result:
|
2023-06-23 16:27:37 +00:00
|
|
|
"Rule match!".debug
|
2023-06-21 03:02:52 +00:00
|
|
|
for filter in rule.filter: msg = msg.filter( filter )
|
|
|
|
|
|
|
|
|
|
var deliver: Maildir
|
|
|
|
|
if rule.deliver != "":
|
|
|
|
|
deliver = default.subDir( rule.deliver )
|
|
|
|
|
else:
|
|
|
|
|
deliver = default
|
|
|
|
|
|
|
|
|
|
msg.save( deliver )
|
2023-06-29 12:58:42 +00:00
|
|
|
return # stop processing additional rules
|
2023-06-21 03:02:52 +00:00
|
|
|
|