From 6033099316d2081a9502bb066908a1ac670e519b Mon Sep 17 00:00:00 2001 From: Stefan Huber Date: Sun, 29 Dec 2013 11:59:16 +0100 Subject: [PATCH] Initial commit --- LICENSE | 165 +++++++++++++++++ Makefile | 28 +++ mailq | 38 ++++ sendmail | 40 +++++ smailq | 472 +++++++++++++++++++++++++++++++++++++++++++++++++ smailq.conf | 21 +++ smailq.docbook | 231 ++++++++++++++++++++++++ 7 files changed, 995 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100755 mailq create mode 100755 sendmail create mode 100755 smailq create mode 100644 smailq.conf create mode 100644 smailq.docbook diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1ca9fc6 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +PREFIX=/usr +# Where the script binary should go +BINPATH = $(PREFIX)/bin +SBINPATH = $(PREFIX)/sbin +MANPATH = $(PREFIX)/share/man +SHAREDIR = $(PREFIX)/share/smailq +CONFFILE = $(SHAREDIR)/smailq.conf.sample + +###################################################################### + +all: manpage + +install: all + install -m 0755 smailq $(BINPATH)/smailq + install -m 0755 mailq $(BINPATH)/mailq + install -m 0755 sendmail $(SBINPATH)/sendmail + mkdir -p $(SHAREDIR) + install -m 0644 smailq.conf $(CONFFILE) + install -m 0644 smailq.1 $(MANPATH)/man1/smailq.1 + bzip2 -f $(MANPATH)/man1/smailq.1 + +manpage: smailq.1 + +smailq.1: smailq.docbook + docbook2man.pl $< + #groff -t -e -mandoc -Tps smailq.1 > smailq.ps + + diff --git a/mailq b/mailq new file mode 100755 index 0000000..c59062a --- /dev/null +++ b/mailq @@ -0,0 +1,38 @@ +#!/bin/sh + +# Copyright (c) 2013 Stefan Huber + +set -e + +PROGNAME=$(basename $0) + +usage() { + cat < + +set -e + +CMD="$(dirname $0)/smailq" +options= +list="0" + +usage() { + cat << EOF +A wrapper script for smailq that behaves like sendmail. + +USAGE: + $0 [OPTIONS] [recipient ...] + $0 --help + +It simply calls '$CMD --send -- [OPTIONS] [recipient ...]'. In +particular, it passes all options as MSA options to smailq. +EOF +} + + +options=() +while [ $# -gt "0" ]; do + if [ "$1" == "-bp" ]; then + list="1" + else + options+=("$1") + fi + shift +done + +if [ "$list" = "1" ]; then + smailq --list +else + smailq --send -- "${options[@]}" +fi + diff --git a/smailq b/smailq new file mode 100755 index 0000000..c42b980 --- /dev/null +++ b/smailq @@ -0,0 +1,472 @@ +#!/usr/bin/env python3 +"""A mail queue for lightweight SMTP clients (MSAs) like msmtp.""" + +__author__ = "Stefan Huber" +__copyright__ = "Copyright 2013" + +__license__ = "LGPL-3" +__version__ = "1.0" + + +from contextlib import contextmanager +import configparser +import fcntl +import getopt +import os +import pickle +import random +import shlex +import subprocess +import sys +import time +import socket + + +verbose = False +quiet = False + + +class Config: + """Configuration read from a config file""" + + class ConfigError(RuntimeError): + """Error when reading config file""" + def __init__(self, value): + self.value = value + self.message = value + + def __init__(self, conffn): + self.logdir = None + self.datadir = None + self.nwtesthost = None + self.nwtestport = None + self.nwtesttimeout = None + self.msacmd = None + + self.__nwtest = None + + self.__read(conffn) + + def __read(self, conffn): + conf = configparser.RawConfigParser() + conf.read(conffn) + + self.logdir = "~/.smailq/log" + self.datadir = "~/.smailq/data" + self.nwtesthost = "www.google.com" + self.nwtestport = 80 + self.nwtesttimeout = 8 + + self.logdir = conf.get("general", "logdir", fallback=self.logdir) + self.datadir = conf.get("general", "datadir", fallback=self.datadir) + self.nwtesthost = conf.get("nwtest", "host", fallback=self.nwtesthost) + self.nwtestport = conf.getint("nwtest", "port", + fallback=self.nwtestport) + self.nwtesttimeout = conf.getint("nwtest", "timeout", + fallback=self.nwtesttimeout) + + if not conf.has_option("msa", "cmd"): + raise Config.ConfigError("Section 'msa' contains no 'cmd' option.") + self.msacmd = conf.get("msa", "cmd") + + def getdatadir(self): + """Returns the directory for the mail data""" + return os.path.expanduser(self.datadir) + + def getlogdir(self): + """Returns the directory for the log data""" + return os.path.expanduser(self.logdir) + + def getlockfn(self): + """Get a lock filename of the data directory""" + return self.getdatadir() + "/.lock" + + def getmailfn(self, id): + return self.getdatadir() + "/" + id + ".eml" + + def getmsaargsfn(self, id): + return self.getdatadir() + "/" + id + ".msaargs" + + @contextmanager + def aquiredatalock(self): + """Get a lock on the data directory""" + fn = self.getlockfn() + + # If lock file exists, wait until it disappears + while os.path.exists(fn): + time.sleep(0.05) + + # Use lockf to get exclusive access to file + fp = open(fn, 'w') + fcntl.lockf(fp, fcntl.LOCK_EX) + try: + yield + finally: + fcntl.lockf(fp, fcntl.LOCK_UN) + fp.close() + os.remove(self.getlockfn()) + + def networktest(self): + """Test if we have connection to the internet.""" + + if self.__nwtest is None: + self.__nwtest = False + try: + host = (self.nwtesthost, self.nwtestport) + to = self.nwtesttimeout + with socket.create_connection(host, timeout=to): + self.__nwtest = True + except OSError as e: + pass + except Exception as e: + printerr(e) + + return self.__nwtest + + +class MailQueue: + + def __init__(self, conf): + self.conf = conf + self.__mailids = None + + def get_mail_ids(self): + """Return a list of all mail IDs""" + + # Get mail and msaargs files in datadir + listdir = os.listdir(self.conf.getdatadir()) + mailfiles = [f for f in listdir if f.endswith(".eml")] + msaargsfiles = [f for f in listdir if f.endswith(".msaargs")] + + # Strip of file endings + mailfiles = [f[:-4] for f in mailfiles] + msaargsfiles = [f[:-8] for f in msaargsfiles] + + # Check if symmetric difference is zero + for f in set(mailfiles) - set(msaargsfiles): + printerr("For ID %s an eml file but no msaargs file exists." % f) + for f in set(msaargsfiles) - set(mailfiles): + printerr("For ID %s a msaargs file but no eml file exists." % f) + + # Get mail IDs + return set(mailfiles) & set(msaargsfiles) + + def getmailinfo(self, id): + """Get some properties of mail with given ID""" + assert(id in self.get_mail_ids()) + + mailfn = self.conf.getmailfn(id) + + info = {} + info['ctime'] = time.ctime(os.path.getctime(mailfn)) + info['size'] = os.path.getsize(mailfn) + + with open(mailfn, "r") as f: + mail = f.readlines() + + for l in mail: + if l.startswith("Subject:"): + info['subject'] = l[8:].strip() + break + + for l in mail: + if l.startswith("To:"): + info['to'] = l[3:].strip() + break + + return info + + def printmailinfo(self, id): + """Print some info on the mail with given ID""" + + print("ID %s:" % id) + + if not id in self.get_mail_ids(): + printerr("ID %s is not in the queue!" % id) + return + + info = self.getmailinfo(id) + + print(" Time: %s" % info['ctime']) + print(" Size: %s Bytes" % info['size']) + print(" To: %s" % info['to']) + print(" Subject: %s" % info['subject']) + + def listqueue(self): + """Print a list of mails in the mail queue""" + + ids = self.get_mail_ids() + print("%d mails in the queue.\n" % len(ids)) + for id in ids: + self.printmailinfo(id) + print() + + def deletemail(self, id): + """Attempt to deliver mail with given ID""" + printinfo("Removing mail with ID " + id) + + if not id in self.get_mail_ids(): + printerr("ID %s is not in the queue!" % id) + return + + os.remove(conf.getmailfn(id)) + os.remove(conf.getmsaargsfn(id)) + log(conf, "Removed from queue.", id=id) + + def delivermail(self, id): + """Attempt to deliver mail with given ID""" + printinfo("Deliver mail with ID " + id) + + if not id in self.get_mail_ids(): + printerr("ID %s is not in the queue!" % id) + return + + if not self.conf.networktest(): + printinfo("Network down. Do not deliver mail.") + return + + info = self.getmailinfo(id) + log(conf, "Attempting to deliver mail. To=%s" % info['to'], id=id) + + # Read the mail + mailfn = self.conf.getmailfn(id) + mailf = open(mailfn, "r") + + # Read the options + msaargsfn = self.conf.getmsaargsfn(id) + msaargs = None + with open(msaargsfn, "rb") as f: + msaargs = pickle.load(f) + + # Build argv for the MSA + msacmd = self.conf.msacmd + msaargv = shlex.split(msacmd) + msaargv += msaargs + + # Call the MSA and give it the mail + printinfo("Calling " + " ".join([shlex.quote(m) for m in msaargv])) + ret = subprocess.call(msaargv, stdin=mailf) + + if ret == 0: + log(conf, "Delivery successful.", id=id) + self.deletemail(id) + else: + log(conf, "Delivery failed with exit code %d." % ret, id=id) + + def delivermails(self): + """Attempt to deliver all mails in the mail queue""" + printinfo("Deliver mails in the queue.") + + if not self.conf.networktest(): + printinfo("Network down. Do not deliver mails.") + return + + for id in self.get_mail_ids(): + self.delivermail(id) + + def enqueuemail(self, mail, msaargs): + """Insert the given mail into the mail queue""" + # Creeate a new ID + id = None + while True: + nibbles = 8 + id = hex(random.getrandbits(4*nibbles))[2:].upper() + while len(id) < nibbles: + id = '0' + id + if not os.path.exists(self.conf.getmailfn(id)): + break + + log(conf, "Insert into queue.", id=id) + + # Write the mail + mailfn = self.conf.getmailfn(id) + with open(mailfn, "w") as f: + f.write(mail) + + # Write the options + msaargsfn = self.conf.getmsaargsfn(id) + with open(msaargsfn, "wb") as f: + pickle.dump(msaargs, f) + + return id + + def sendmail(self, mail, msaargs): + """Insert a mail in the mail queue, and attempt to deliver mails""" + self.enqueuemail(mail, msaargs) + self.delivermails() + + +def log(conf, msg, id=None): + """Write message to log file""" + fn = conf.getlogdir() + "/smailq.log" + + with open(fn, 'a') as f: + fcntl.lockf(f, fcntl.LOCK_EX) + # Prepend ID to msg + if id is not None: + msg = ("ID %s: " % id) + msg + # Prepend time to msg + msg = time.strftime("%Y-%m-%d %H:%M:%S: ", time.localtime()) + msg + + # Write msg line + f.write(msg + "\n") + if not quiet: + print(msg) + + fcntl.lockf(f, fcntl.LOCK_UN) + + +def printerr(msg): + """Print an error message""" + print(msg, file=sys.stderr) + + +def printinfo(msg): + if verbose: + print(msg) + + +def version(): + """Show version info""" + + print("smailq " + __version__) + print("Copyright (C) 2013 Stefan Huber") + + +def usage(): + """Print usage text of this program""" + + print(""" +smailq is a mail queue for lightweight SMTP clients (MSAs) like msmtp that do +not provide a queue. It basically provides the functionality of sendmail and +mailq. + +USAGE: + + {0} --send [recipient ...] -- [MSA options ...] + {0} --list + {0} --deliver-all + {0} --deliver [ID ...] + {0} --delete [ID ...] + {0} --help + {0} --version + +COMMANDS: + + --delete + Remove the mails with given IDs from the queue. + + --deliver + Attempt to deliver the mails with given IDs only. + + --deliver-all + Attempt to deliver all mails in the queue. + + -h, --help + Print this usage text. + + --list + List all mails in the queue. This is the default + + --send + Read a mail from stdin, insert it into the queue, and attempt to + deliver all mails in the queue. Options after "--" are passed forward + to the MSA for this particular mail. + + -V, --version + Show version info. + +OPTIONS: + + -C, --config=FILE + Use the given configuration file instead of "$HOME/.smailq.conf". + + -q, --quiet + Do not print info messages. + + -v, --verbose + Increase output verbosity. +""".format(sys.argv[0])) + + +if __name__ == "__main__": + + conffn = os.path.expanduser("~/.smailq.conf") + cmd = "--list" + nooptargs = [] + + try: + + longopts = ["config=", "delete", "deliver-all", "deliver", "help", + "list", "send", "verbose", "version", "quiet"] + opts, nooptargs = getopt.gnu_getopt(sys.argv[1:], "hC:vVq", longopts) + + for opt, arg in opts: + if opt in ['-h', '--help']: + usage() + sys.exit(os.EX_OK) + elif opt in ['-V', '--version']: + version() + sys.exit(os.EX_OK) + elif opt in ['--list', '--send', '--delete', '--deliver-all', + '--deliver']: + cmd = opt + elif opt in ['-C', '--config']: + conffn = arg + elif opt in ['-v', '--verbose']: + verbose = True + quiet = False + elif opt in ['-q', '--quiet']: + quiet = True + verbose = False + else: + assert(False) + + except getopt.GetoptError as e: + printerr("Error parsing arguments:", e) + usage() + sys.exit(os.EX_USAGE) + + # Reading config file + if not os.path.isfile(conffn): + printerr("No such config file:", conffn) + sys.exit(os.EX_IOERR) + conf = None + try: + conf = Config(conffn) + + if not os.path.isdir(conf.getdatadir()): + printerr("Data directory does not exist: " + conf.getdatadir()) + sys.exit(os.EX_IOERR) + + if not os.path.isdir(conf.getlogdir()): + printerr("Log directory does not exist: " + conf.getlogdir()) + sys.exit(os.EX_IOERR) + + except Exception as e: + printerr("Error reading config file:", e) + sys.exit(os.EX_IOERR) + + try: + with conf.aquiredatalock(): + + printinfo("Aquired the lock.") + + mq = MailQueue(conf) + if cmd == "--send": + mail = sys.stdin.read() + mq.sendmail(mail, nooptargs) + elif cmd == "--list": + mq.listqueue() + elif cmd == "--deliver-all": + mq.delivermails() + elif cmd == "--deliver": + for id in nooptargs: + mq.delivermail(id) + elif cmd == "--delete": + for id in nooptargs: + mq.deletemail(id) + + except OSError as e: + printerr(e) + sys.exit(os.EX_IOERR) diff --git a/smailq.conf b/smailq.conf new file mode 100644 index 0000000..1e40437 --- /dev/null +++ b/smailq.conf @@ -0,0 +1,21 @@ +# General settings concerning smailq +[general] +# Optional: The directory where the log file is written to +logdir = ~/.smailq/log/ +# Optional: The directory where smailq saves the mail queue data +datadir = ~/.smailq/data/ + +# Settings for the network (TCP) connectivity test +[nwtest] +# Optional: The host to connect to +host = www.google.com +# Optional: The port to connect to +port = 80 +# Optional: The timeout +timeout = 8 + +# Settings concerning the mail submission agent +[msa] +# This command is called when smailq attempts to deliver a mail. The +# MSA-options passed to smailq are appended to this line. +cmd = /usr/bin/msmtp diff --git a/smailq.docbook b/smailq.docbook new file mode 100644 index 0000000..1e28c0b --- /dev/null +++ b/smailq.docbook @@ -0,0 +1,231 @@ + + + + + + + +
+ shuber@sthu.org +
+ + Stefan + Huber + + 2013-12-29 +
+ + + smailq + 1 + smailq 1.0q + + + + smailq + A simple mail queue. + + + + + smailq + + recipient + + MSA options + + + smailq + + + + smailq + + + + smailq + + ID + + + smailq + + ID + + + smailq + + + + smailq + + + + + + DESCRIPTION + + + smailq is a mail queue for lightweight SMTP + clients (MSAs) like msmtp that do not provide a queue. It basically + provides the functionality of sendmail and mailq. + + + + When smailq sends a mail it first inserts the + mail into the message queue and then attempts to deliver all mails + in the queue using an external MSA such as msmtp. + + + + + + COMMANDS + + + + + + + Remove the mails with given IDs from the queue. + + + + + + + Attempt to deliver the mails with given IDs only. + + + + + + + Attempt to deliver all mails in the queue. + + + + + + + + Print usage text. + + + + + + + List all mails in the queue. This is the default. + + + + + + + Read a mail from stdin, insert it into the queue, and + attempt to deliver all mails in the queue. Options after + "--" are passed forward to the MSA for this particular + mail. + + + + + + + + Show version info. + + + + + + + + + OPTIONS + + + + + + + + Use the given configuration file instead of + "$HOME/.smailq.conf". + + + + + + + + Do not print info messages. + + + + + + + + Increase output verbosity. + + + + + + + + CONFIGURATION FILES + + + By default, smailq reads $HOME/.smailq.conf as + configuration file. The syntax follows RFC 822. A sample + configuration file contains the following lines: + + + +[general] +# Optional: The directory where the log file is written to +logdir = ~/.smailq/log/ +# Optional: The directory where smailq saves the mail queue data +datadir = ~/.smailq/data/ + +# Settings for the network (TCP) connectivity test +[nwtest] +# Optional: The host to connect to +host = www.google.com +# Optional: The port to connect to +port = 80 +# Optional: The timeout +timeout = 8 + +# Settings concerning the mail submission agent +[msa] +# This command is called when smailq attempts to deliver a mail. The +# MSA-options passed to smailq are appended to this line. +cmd = /usr/bin/msmtp + + + + + + BUGS + Bug reports to Stefan Huber shuber@sthu.org. + + + + AUTHOR + + + + Stefan + Huber + Original author + shuber@sthu.org + + + +
-- 2.39.5