diff --git a/man/man1/putmail.py.1 b/man/man1/putmail.py.1 index 286f286..4a70047 100644 --- a/man/man1/putmail.py.1 +++ b/man/man1/putmail.py.1 @@ -99,21 +99,34 @@ The password used when authenticating to the SMTP server. The server port. The default value is 25. .TP \fItls\fR -Boolean option to enable or disable TLS. It is disabled by -default or when its value is \fIno\fR, \fI0\fR, \fIoff\fR -or \fIfalse\fR. Set it to \fIyes\fR, \fI1\fR, \fIon\fR -or \fItrue\fR to enable TLS. +The TLS mode to use, disabled by default or when set to +\fIoff\fR. Set it to \fInative\fR or \fIstarttls\fR to use TLS. +When active, \fBputmail.py\fR will refuse to authenticate or +send mail without TLS. +.TP +\fItls_versions\fR +A whitespace-separated list of TLS versions to use. Ignored +when \fItls\fR is inactive. Known values: \fIssl2\fR, +\fIssl3\fR, \fItls1\fR, \fItls1_1\fR, \fItls1_2\fR. Note that +\fBputmail.py\fR cannot use a TLS version unless it is also +supported by the underlying Python implementation (and, by +extension, the underlying OpenSSL implementation). The default +depends on the Python implementation; at the time of +publication, it is recommended that this option be set +manually and include only TLS 1.1 and newer. .TP \fIquiet\fR Boolean option, disabled by default. When active, errors and warnings related to communication with the SMTP server -will not be printed on screen. See the \fItls\fR option -for possible values. Use it with care and once you know -your configuration works. In most cases this will not be -needed. However, sometimes warnings about improper -connection shutdown can get really annoying. Errors -related to malformed configuration files, absence of -message recipients and others will still be printed. +will not be printed on screen. It is disabled by default or +when its value is \fIno\fR, \fI0\fR, \fIoff\fR or \fIfalse\fR. +Set it to \fIyes\fR, \fI1\fR, \fIon\fR or \fItrue\fR to enable +TLS. Use it with care and once you know your configuration +works. In most cases this will not be needed. However, +sometimes warnings about improper connection shutdown can get +really annoying. Errors related to malformed configuration +files, absence of message recipients and others will still be +printed. .SH "COMMAND LINE OPTIONS" .LP .TP diff --git a/putmail.py b/putmail.py index 8e6076a..664529a 100755 --- a/putmail.py +++ b/putmail.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# putmail.py Send mail read from standard input. +# putmail.py Send mail read from standard input. # # Copyright 2007 Ricardo Garcia Gonzalez: http://sourceforge.net/users/rg3/ # @@ -14,12 +14,13 @@ __version_info__ = (1, 4, 1) __version__ = '.'.join([str(i) for i in __version_info__]) -import ConfigParser +import configparser import smtplib import email import os import sys import socket +import ssl import datetime import subprocess as sb @@ -42,9 +43,10 @@ OPTION_SERVER = 'server' OPTION_EMAIL = 'email' OPTION_TLS = 'tls' +OPTION_TLS_VERSIONS = 'tls_versions' OPTION_LOGIN = 'username' OPTION_PASSWORD = 'password' -OPTION_KEYCHAIN= 'keychain' +OPTION_KEYCHAIN = 'keychain' OPTION_PORT = 'port' OPTION_QUIET = 'quiet' @@ -59,6 +61,23 @@ email = your.email@example.com """ +TLS_MODE_NATIVE = 'native' +TLS_MODE_STARTTLS = 'starttls' + +NO_TLS_VERSION_FLAGS = {} +for config_option, attribute in { + 'ssl2': 'OP_NO_SSLv2', + 'ssl3': 'OP_NO_SSLv3', + 'tls1': 'OP_NO_TLSv1', + 'tls1_1': 'OP_NO_TLSv1_1', + 'tls1_2': 'OP_NO_TLSv1_2', + 'tls1_3': 'OP_NO_TLSv1_3', # FutureProofing[tm] + }.items(): + try: + NO_TLS_VERSION_FLAGS[config_option] = getattr(ssl, attribute) + except AttributeError: + pass + # Internal errors ERROR_HOME_UNSET = 'Error: %s environment variable not set' % HOME_EV ERROR_CONFIG_NONEXISTANT = 'Error: config file %s not present' @@ -69,11 +88,13 @@ ERROR_READ_MAIL = 'Error: parse error reading the mail message' ERROR_NO_RECIPIENTS = 'Error: no recipients for the message' ERROR_TLS = 'Error: malformed option "%s"' % OPTION_TLS +ERROR_TLS_VERSION = 'Error: malformed option "%s"' % OPTION_TLS_VERSIONS ERROR_PORT = 'Error: malformed option "%s"' % OPTION_PORT ERROR_QUIET = 'Error: malformed option "%s"' % OPTION_QUIET ERROR_O_OPTION = 'Error: missing option for -o' ERROR_OPTION_ARGS = 'Error: missing arguments for last option' ERROR_UNKNOWN_OPTION = 'Error: unknown option "%s"' +ERROR_OLD_OPENSSL = 'Error: openssl is too far out of date. Please update to at least 0.9.8m' # SMTP and network errors ERROR_REFUSED = 'Error: all recipients rejected by server' @@ -91,64 +112,69 @@ WARNING_REJECTED = 'Warning: the following recipients were rejected:' WARNING_QUIT = 'Warning: problem disconnecting but message probably sent' WARNING_LOGFILE = 'Warning: unable to write to log file' -WARNING_CONFIG_UNREADABLE = 'Warning: config file for %s present but no read access, falling back to default config file' -WARNING_SAMPLE_CONFIG = 'Warning: trying to create sample config file (read manpage and check permissions' +WARNING_CONFIG_UNREADABLE = ( + 'Warning: config file for %s present but no read access, ' + 'falling back to default config file') +WARNING_SAMPLE_CONFIG = ( + 'Warning: trying to create sample config file ' + '(read manpage and check permissions)') # Codes EXIT_FAILURE = 1 ### Some key variables in the program ### -theEMailAddress = None # Envelope From address -theSMTPServer = None # SMTP server to use -thePort = None # The SMTP port in the server -theMessage = None # The E-Mail message -theRecipients = [] # The recipients of the E-Mail message -theConfigFilename = None # The configuration file name -theTLSFlag = False # Use TLS or not -theAuthenticateFlag = False # Use SMTP authentication or not -theSMTPLogin = None # The login name to use with the server -theSMTPPassword = None # The corresponding password -theRcpsFromMailFlag = False # Take recipients from message or not -theQuietFlag = False # Supress program output or not -theLogFile = None # The log file name +theEMailAddress = None # Envelope From address +theSMTPServer = None # SMTP server to use +thePort = None # The SMTP port in the server +theMessage = None # The E-Mail message +theRecipients = [] # The recipients of the E-Mail message +theConfigFilename = None # The configuration file name +theTLSMode = None # Use native TLS, STARTTLS, or neither +theTLSContext = ssl.create_default_context() +theAuthenticateFlag = False # Use SMTP authentication or not +theSMTPLogin = None # The login name to use with the server +theSMTPPassword = None # The corresponding password +theRcpsFromMailFlag = False # Take recipients from message or not +theQuietFlag = False # Supress program output or not +theLogPath = None # The log file name ### A few auxiliary functions ### -def handle_error(str, stderr_output, exit_program): - fullstr = str + "\n" - try: - file(theLogFile, "a").write("%s: %s" % - (datetime.datetime.ctime(datetime.datetime.now()), fullstr)) - except (IOError, OSError): - sys.stderr.write(WARNING_LOGFILE + "\n") +def handle_error(error_string, stderr_output, exit_program): + try: + with open(theLogPath, "a") as theLogFile: + theLogFile.write("%s: %s\n" % ( + datetime.datetime.now().ctime(), error_string)) + except (IOError, OSError): + print(WARNING_LOGFILE, file=sys.stderr) - if stderr_output: - sys.stderr.write(fullstr) - if exit_program: - sys.exit(EXIT_FAILURE) + if stderr_output: + print(error_string, file=sys.stderr) + if exit_program: + sys.exit(EXIT_FAILURE) -def exit_forcing_print(str): - handle_error(str, True, True) +def exit_forcing_print(string): + handle_error(string, True, True) -def exit_conditional_print(str): - handle_error(str, not theQuietFlag, True) +def exit_conditional_print(string): + handle_error(string, not theQuietFlag, True) -def conditional_print(str): - handle_error(str, not theQuietFlag, False) +def conditional_print(string): + handle_error(string, not theQuietFlag, False) -def force_print(str): - handle_error(str, True, False) +def force_print(string): + handle_error(string, True, False) -def check_status((code, message)): - if code >= FIRST_ERROR_CODE: - exit_conditional_print(ERROR_OTHER % (code, message)) +def check_status(code, message): + if code >= FIRST_ERROR_CODE: + exit_conditional_print(ERROR_OTHER % (code, message)) def keychain(keychainType): - if keychainType == 'osx': - return osxkeychain + if keychainType == 'osx': + return osxkeychain -def osxkeychain(service, type="internet"): - cmd = """/usr/bin/security find-%s-password -gs %s""" % (type, service) +def osxkeychain(service, passwordType="internet"): + cmd = """/usr/bin/security find-%s-password -gs %s""" % (passwordType, service) args = shlex.split(cmd) t = sb.check_output(args, stderr=sb.STDOUT) lines = t.split('\n') @@ -157,317 +183,335 @@ def osxkeychain(service, type="internet"): return passwd ### Check for HOME present (needed later, checking now saves a lot of work) ### -if not os.environ.has_key(HOME_EV): - # Note: I still can't use exit_forcing_print() at this point, the log - # filename is not set. - sys.exit(ERROR_HOME_UNSET + "\n") +if not HOME_EV in os.environ: + # Note: I still can't use exit_forcing_print() at this point, the log + # filename is not set. + sys.exit(ERROR_HOME_UNSET + "\n") ### Build the log filename now ### -theLogFile = os.path.join(os.environ[HOME_EV], CONFIG_DIRECTORY, LOG_FILE) +theLogPath = os.path.join(os.environ[HOME_EV], CONFIG_DIRECTORY, LOG_FILE) ############################################### # First step: Parse the command line options. # ############################################### -### This is the new manual option parser to fully comply with ### -### the braindead traditional sendmail options format >:( ### +### This is the new manual option parser to fully comply with ### +### the braindead traditional sendmail options format >:( ### try: - program_args = sys.argv[1:] # Program arguments - print_info = False # prints program information - direct_exit = False # Exit directly? - - ### The lists below contain IGNORED OPTIONS ONLY, see the loop. ### - ### Option groups ending with _exit make the program exit ### - ### directly at the end of the parsing loop ### - - single_options = ['-Ac', '-Am', '-ba', '-bm', '-bs', '-bt', - '-bv', '-G', '-i', '-n', '-v'] - - single_options_exit = ['-bd', '-bD', '-bh', '-bH', '-bp', '-bP', '-qf', - '-bi' ] - - one_argument_options = [ '-B', '-C', '-D', '-d', '-F', '-h', '-L', '-N', - '-O', '-p', '-R', '-r', '-V', '-X' ] - - one_argument_options_exit = ['-qG', '-qI', '-qQ', '-qR', '-qS', '-q!I', - '-q!Q', '-q!R', '-q!S' ] - - optional_extra_chars_options_exit = [ '-q', '-qp', '-Q' ] - - # Eat arguments until there's none left - while (len(program_args) > 0): - if program_args[0] == '--': # Remaining options are recipients - theRecipients.extend(program_args[1:]) - break - elif program_args[0] == '--version': - print_info = True - direct_exit = True - break - elif program_args[0] == '-t': # Take recipients from message - theRcpsFromMailFlag = True - del program_args[0] - elif program_args[0] in single_options: - del program_args[0] - elif program_args[0] in single_options_exit: - del program_args[0] - direct_exit = True - elif program_args[0] == '-f': # Address in next argument - theEMailAddress = program_args[1] - del program_args[0] - del program_args[0] - elif program_args[0][0:2] == '-f': # Address in this argument - theEMailAddress = program_args[0][2:] - del program_args[0] - elif program_args[0] in one_argument_options: - del program_args[0] - del program_args[0] - elif program_args[0][0:2] in one_argument_options: - del program_args[0] - elif program_args[0] in one_argument_options_exit: - del program_args[0] - del program_args[0] - direct_exit = True - elif (sum([program_args[0].startswith(x) # First chars match - for x in one_argument_options_exit]) > 0): - del program_args[0] - direct_exit = True - elif (sum([program_args[0].startswith(x) # First chars match - for x in optional_extra_chars_options_exit]) > 0): - del program_args[0] - direct_exit = True - elif program_args[0].startswith('-o'): # Weird case - if program_args[0] == '-o': - exit_forcing_print(ERROR_O_OPTION) - del program_args[0] - del program_args[0] - else: - if program_args[0].startswith('-'): - exit_forcing_print(ERROR_UNKNOWN_OPTION % - program_args[0]) - theRecipients.append(program_args[0]) - del program_args[0] - # End of parsing loop + program_args = sys.argv[1:] # Program arguments + print_info = False # prints program information + direct_exit = False # Exit directly? + + ### The lists below contain IGNORED OPTIONS ONLY, see the loop. ### + ### Option groups ending with _exit make the program exit ### + ### directly at the end of the parsing loop ### + + single_options = ['-Ac', '-Am', '-ba', '-bm', '-bs', '-bt', + '-bv', '-G', '-i', '-n', '-v'] + + single_options_exit = ['-bd', '-bD', '-bh', '-bH', '-bp', '-bP', '-qf', + '-bi'] + + one_argument_options = ['-B', '-C', '-D', '-d', '-F', '-h', '-L', '-N', + '-O', '-p', '-R', '-r', '-V', '-X'] + + one_argument_options_exit = ['-qG', '-qI', '-qQ', '-qR', '-qS', '-q!I', + '-q!Q', '-q!R', '-q!S'] + + optional_extra_chars_options_exit = ['-q', '-qp', '-Q'] + + # Eat arguments until there's none left + while program_args: + if program_args[0] == '--': # Remaining options are recipients + theRecipients.extend(program_args[1:]) + break + elif program_args[0] == '--version': + print_info = True + direct_exit = True + break + elif program_args[0] == '-t': # Take recipients from message + theRcpsFromMailFlag = True + del program_args[0] + elif program_args[0] in single_options: + del program_args[0] + elif program_args[0] in single_options_exit: + del program_args[0] + direct_exit = True + elif program_args[0] == '-f': # Address in next argument + theEMailAddress = program_args[1] + del program_args[0] + del program_args[0] + elif program_args[0][0:2] == '-f': # Address in this argument + theEMailAddress = program_args[0][2:] + del program_args[0] + elif program_args[0] in one_argument_options: + del program_args[0] + del program_args[0] + elif program_args[0][0:2] in one_argument_options: + del program_args[0] + elif program_args[0] in one_argument_options_exit: + del program_args[0] + del program_args[0] + direct_exit = True + elif any(program_args[0].startswith(x) + for x in one_argument_options_exit): + del program_args[0] + direct_exit = True + elif any(program_args[0].startswith(x) + for x in optional_extra_chars_options_exit): + del program_args[0] + direct_exit = True + elif program_args[0].startswith('-o'): # Weird case + if program_args[0] == '-o': + exit_forcing_print(ERROR_O_OPTION) + del program_args[0] + del program_args[0] + else: + if program_args[0].startswith('-'): + exit_forcing_print(ERROR_UNKNOWN_OPTION % + program_args[0]) + theRecipients.append(program_args[0]) + del program_args[0] + # End of parsing loop # Problem in some option argument except IndexError: - exit_forcing_print(ERROR_OPTION_ARGS) + exit_forcing_print(ERROR_OPTION_ARGS) # print program info if print_info: - programName = os.path.basename(sys.argv[0]) - version = "%s %s" % (programName, __version__) - print version - print " type `man %s` for more information" % programName + programName = os.path.basename(sys.argv[0]) + version = "%s %s" % (programName, __version__) + print(version) + print(" type `man %s` for more information" % programName) # Options indicated direct exit if direct_exit: - sys.exit() + sys.exit() # No addresses found? -if len(theRecipients) == 0 and not theRcpsFromMailFlag: - exit_forcing_print(ERROR_NO_RECIPIENTS) +if not theRecipients and not theRcpsFromMailFlag: + exit_forcing_print(ERROR_NO_RECIPIENTS) ###################################################### # Second step: Read the message from standard input. # ###################################################### try: - theMessage = email.message_from_file(sys.stdin) + theMessage = email.message_from_file(sys.stdin) except email.Errors.MessageError: - exit_forcing_print(ERROR_READ_MAIL) + exit_forcing_print(ERROR_READ_MAIL) ############################################ # Third step: Read the configuration file. # ############################################ -### Try to find the apropiate configuration file or ### -### fall back to CONFIG_NAME. ### +### Try to find the apropiate configuration file or ### +### fall back to CONFIG_NAME. ### configPath = os.path.join(os.environ[HOME_EV], CONFIG_DIRECTORY) # temporally theConfigFilename = CONFIG_NAME -if theMessage.has_key(FROM_HEADER): - try: - fromaddr = email.Utils.getaddresses( - theMessage.get_all(FROM_HEADER))[-1][1] - tmpcfgpath = os.path.join(configPath, fromaddr) - if os.path.isfile(tmpcfgpath): - if os.access(tmpcfgpath, os.R_OK): - theConfigFilename = fromaddr - else: - force_print(WARNING_CONFIG_UNREADABLE %fromaddr) - - except IndexError: - pass +if FROM_HEADER in theMessage: + try: + fromaddr = email.utils.getaddresses( + theMessage.get_all(FROM_HEADER))[-1][1] + tmpcfgpath = os.path.join(configPath, fromaddr) + if os.path.isfile(tmpcfgpath): + if os.access(tmpcfgpath, os.R_OK): + theConfigFilename = fromaddr + else: + force_print(WARNING_CONFIG_UNREADABLE %fromaddr) + + except IndexError: + pass configPath = os.path.join(configPath, theConfigFilename) # finally if not os.path.exists(configPath): - # Config file not present, try to create one and exit - force_print(ERROR_CONFIG_NONEXISTANT % configPath) - force_print(WARNING_SAMPLE_CONFIG) + # Config file not present, try to create one and exit + force_print(ERROR_CONFIG_NONEXISTANT % configPath) + force_print(WARNING_SAMPLE_CONFIG) - try: - dirname = os.path.dirname(configPath) - if not os.path.isdir(dirname): - os.makedirs(dirname) - file(configPath, "w").write(DEFAULT_CONFIG) - except: - exit_forcing_print(ERROR_CONFIG_CREATE) + try: + dirname = os.path.dirname(configPath) + if not os.path.isdir(dirname): + os.makedirs(dirname) + with open(configPath, "w") as configFile: + configFile.write(DEFAULT_CONFIG) + except OSError: + exit_forcing_print(ERROR_CONFIG_CREATE) - sys.exit(EXIT_FAILURE) + sys.exit(EXIT_FAILURE) # Last check. If we cannot read this we cannot proceed. if not os.access(configPath, os.R_OK): - exit_forcing_print(ERROR_CONFIG_UNREADABLE) + exit_forcing_print(ERROR_CONFIG_UNREADABLE) ### Read the file ### -config = ConfigParser.ConfigParser() +config = configparser.ConfigParser() try: - config.read([configPath]) -except: - exit_forcing_print(ERROR_CONFIG_PARSE) + config.read([configPath]) +except OSError: + exit_forcing_print(ERROR_CONFIG_PARSE) ### Check conditions for bad configurations ### if (not config.has_section(CONFIG_SECTION) or - not config.has_option(CONFIG_SECTION, OPTION_SERVER) or - not config.has_option(CONFIG_SECTION, OPTION_EMAIL) or - (config.has_option(CONFIG_SECTION, OPTION_LOGIN) and - not (config.has_option(CONFIG_SECTION, OPTION_PASSWORD) or - config.has_option(CONFIG_SECTION, OPTION_KEYCHAIN))) or - ((config.has_option(CONFIG_SECTION, OPTION_PASSWORD) or - config.has_option(CONFIG_SECTION, OPTION_KEYCHAIN)) and not - config.has_option(CONFIG_SECTION, OPTION_LOGIN))): - exit_forcing_print(ERROR_CONFIG_PARSE) - -### Extract the necessary configuration parameters ## + not config.has_option(CONFIG_SECTION, OPTION_SERVER) or + not config.has_option(CONFIG_SECTION, OPTION_EMAIL) or + (config.has_option(CONFIG_SECTION, OPTION_LOGIN) and + not (config.has_option(CONFIG_SECTION, OPTION_PASSWORD) or + config.has_option(CONFIG_SECTION, OPTION_KEYCHAIN))) or + ((config.has_option(CONFIG_SECTION, OPTION_PASSWORD) or + config.has_option(CONFIG_SECTION, OPTION_KEYCHAIN)) and not + config.has_option(CONFIG_SECTION, OPTION_LOGIN))): + exit_forcing_print(ERROR_CONFIG_PARSE) + +### Extract the necessary configuration parameters ### theSMTPServer = config.get(CONFIG_SECTION, OPTION_SERVER) -if theEMailAddress is None: # "Envelope from" if -f was not present - theEMailAddress = config.get(CONFIG_SECTION, OPTION_EMAIL) - -try: # TLS - if (config.has_option(CONFIG_SECTION, OPTION_TLS) and - config.getboolean(CONFIG_SECTION, OPTION_TLS)): - theTLSFlag = True -except ValueError: - exit_forcing_print(ERROR_TLS) - -try: # Quiet - if (config.has_option(CONFIG_SECTION, OPTION_QUIET) and - config.getboolean(CONFIG_SECTION, OPTION_QUIET)): - theQuietFlag = True +if theEMailAddress is None: # "Envelope from" if -f was not present + theEMailAddress = config.get(CONFIG_SECTION, OPTION_EMAIL) + +if config.has_option(CONFIG_SECTION, OPTION_TLS): # TLS + theTLSMode = config.get(CONFIG_SECTION, OPTION_TLS) + try: + if not config.getboolean(CONFIG_SECTION, OPTION_TLS): + theTLSMode = None + except ValueError: + pass + if theTLSMode not in {TLS_MODE_NATIVE, TLS_MODE_STARTTLS, None}: + exit_forcing_print(ERROR_TLS) + +if config.has_option(CONFIG_SECTION, OPTION_TLS_VERSIONS): # TLS versions + for flag in NO_TLS_VERSION_FLAGS.values(): + theTLSContext.options |= flag + for version in config.get(CONFIG_SECTION, OPTION_TLS_VERSIONS).split(): + try: + theTLSContext.options &= ~NO_TLS_VERSION_FLAGS[version] + except KeyError: + exit_forcing_print(ERROR_TLS_VERSION) + # openssls older than 0.9.8m will cause a ValueError + # to be raised when we try to unset an option + except ValueError: + exit_forcing_print(ERROR_OLD_OPENSSL) + +try: # Quiet + if (config.has_option(CONFIG_SECTION, OPTION_QUIET) and + config.getboolean(CONFIG_SECTION, OPTION_QUIET)): + theQuietFlag = True except ValueError: - exit_forcing_print(ERROR_QUIET) - -if config.has_option(CONFIG_SECTION, OPTION_LOGIN): # Login/password - theSMTPLogin = config.get(CONFIG_SECTION, OPTION_LOGIN) - try: - # if config.has_option(CONFIG_SECTION, OPTION_KEYCHAIN): - keychainType = config.get(CONFIG_SECTION, OPTION_KEYCHAIN) - keychain_func = keychain(keychainType) - theSMTPPassword = keychain_func(theSMTPServer) - except TypeError: - exit_forcing_print(ERROR_CONFIG_KEYCHAIN) - except ConfigParser.NoOptionError: - theSMTPPassword = config.get(CONFIG_SECTION, OPTION_PASSWORD) - theAuthenticateFlag = True - -try: # Port - if config.has_option(CONFIG_SECTION, OPTION_PORT): - thePort = config.getint(CONFIG_SECTION, OPTION_PORT) - if thePort < 0 or thePort > HIGHEST_PORT: - raise ValueError - else: - thePort = DEFAULT_PORT + exit_forcing_print(ERROR_QUIET) + +if config.has_option(CONFIG_SECTION, OPTION_LOGIN): # Login/password + theSMTPLogin = config.get(CONFIG_SECTION, OPTION_LOGIN) + try: + # if config.has_option(CONFIG_SECTION, OPTION_KEYCHAIN): + keychainType = config.get(CONFIG_SECTION, OPTION_KEYCHAIN) + keychain_func = keychain(keychainType) + theSMTPPassword = keychain_func(theSMTPServer) + except TypeError: + exit_forcing_print(ERROR_CONFIG_KEYCHAIN) + except configparser.NoOptionError: + theSMTPPassword = config.get(CONFIG_SECTION, OPTION_PASSWORD) + theAuthenticateFlag = True + +try: # Port + if config.has_option(CONFIG_SECTION, OPTION_PORT): + thePort = config.getint(CONFIG_SECTION, OPTION_PORT) + if thePort < 0 or thePort > HIGHEST_PORT: + raise ValueError() + else: + thePort = DEFAULT_PORT except ValueError: - exit_forcing_print(ERROR_PORT) + exit_forcing_print(ERROR_PORT) ########################################################################## -# Fourth step: Extract information from important headers (like To) and # -# remove the Bcc header from the message. # +# Fourth step: Extract information from important headers (like To) and # +# remove the Bcc header from the message. # ########################################################################## # If we are told to take the addresses from the message itself... if theRcpsFromMailFlag: - if theMessage.has_key(TO_HEADER): - theRecipients.extend( - [x[1] for x in email.Utils.getaddresses( - theMessage.get_all(TO_HEADER))] - ) - if theMessage.has_key(CC_HEADER): - theRecipients.extend( - [x[1] for x in email.Utils.getaddresses( - theMessage.get_all(CC_HEADER))] - ) - if theMessage.has_key(BCC_HEADER): - theRecipients.extend( - [x[1] for x in email.Utils.getaddresses( - theMessage.get_all(BCC_HEADER))] - ) + if TO_HEADER in theMessage: + theRecipients.extend( + [x[1] for x in email.utils.getaddresses( + theMessage.get_all(TO_HEADER))] + ) + if CC_HEADER in theMessage: + theRecipients.extend( + [x[1] for x in email.utils.getaddresses( + theMessage.get_all(CC_HEADER))] + ) + if BCC_HEADER in theMessage: + theRecipients.extend( + [x[1] for x in email.utils.getaddresses( + theMessage.get_all(BCC_HEADER))] + ) # Delete Bcc header if it exists -if theMessage.has_key(BCC_HEADER): - del theMessage[BCC_HEADER] +if BCC_HEADER in theMessage: + del theMessage[BCC_HEADER] # Still no addresses found? -if len(theRecipients) == 0: - exit_forcing_print(ERROR_NO_RECIPIENTS) +if not theRecipients: + exit_forcing_print(ERROR_NO_RECIPIENTS) #################################################### # Fifth step: Send the damned message finally \o/. # #################################################### try: - server = smtplib.SMTP() - #server.set_debuglevel(True) - check_status(server.connect(theSMTPServer, thePort)) - check_status(server.ehlo()) - if theTLSFlag: - check_status(server.starttls()) - check_status(server.ehlo()) # Repeat EHLO after starting TLS - if theAuthenticateFlag: - check_status(server.login(theSMTPLogin, theSMTPPassword)) - rejected = server.sendmail(theEMailAddress, theRecipients, - theMessage.as_string()) + if theTLSMode == TLS_MODE_NATIVE: + server = smtplib.SMTP_SSL(theSMTPServer, thePort, + context=theTLSContext) + else: + server = smtplib.SMTP(theSMTPServer, thePort) + #server.set_debuglevel(True) + check_status(*server.ehlo('0.0.0.0')) + if theTLSMode == TLS_MODE_STARTTLS: + check_status(*server.starttls(context=theTLSContext)) + # SMTP.starttls() discards all knowledge obtained from the server + # as per RFC 3207. This means we need to EHLO again. + check_status(*server.ehlo('0.0.0.0')) + if theAuthenticateFlag: + check_status(*server.login(theSMTPLogin, theSMTPPassword)) + rejectedRecipients = server.sendmail(theEMailAddress, theRecipients, + theMessage.as_string()) except smtplib.SMTPServerDisconnected: - exit_conditional_print(ERROR_DISCONNECTED) + exit_conditional_print(ERROR_DISCONNECTED) except smtplib.SMTPSenderRefused: - exit_conditional_print(ERROR_SENDER_REFUSED) + exit_conditional_print(ERROR_SENDER_REFUSED) except smtplib.SMTPRecipientsRefused: - exit_conditional_print(ERROR_REFUSED) + exit_conditional_print(ERROR_REFUSED) except smtplib.SMTPDataError: - exit_conditional_print(ERROR_DATA) + exit_conditional_print(ERROR_DATA) except smtplib.SMTPConnectError: - exit_conditional_print(ERROR_CONNECT) + exit_conditional_print(ERROR_CONNECT) except smtplib.SMTPHeloError: - exit_conditional_print(ERROR_HELO) + exit_conditional_print(ERROR_HELO) except smtplib.SMTPAuthenticationError: - exit_conditional_print(ERROR_AUTH) -except smtplib.SMTPResponseException, err: - exit_conditional_print(ERROR_OTHER % (err.smtp_code, err.smtp_error)) -except (socket.error, socket.herror, socket.gaierror), err: - exit_conditional_print(ERROR_NETWORK % (theSMTPServer, err[1])) -except socket.timeout, err: - exit_conditional_print(ERROR_NETWORK % (theSMTPServer, err)) + exit_conditional_print(ERROR_AUTH) +except smtplib.SMTPResponseException as err: + exit_conditional_print(ERROR_OTHER % (err.smtp_code, err.smtp_error)) +except socket.timeout as err: + exit_conditional_print(ERROR_NETWORK % (theSMTPServer, err)) except smtplib.SMTPException: - exit_conditional_print(ERROR_UNKNOWN) + exit_conditional_print(ERROR_UNKNOWN) +except (socket.herror, socket.gaierror, OSError) as err: + exit_conditional_print(ERROR_NETWORK % (theSMTPServer, str(err))) try: - server.quit() -except: - conditional_print(WARNING_QUIT) + server.quit() +except smtplib.SMTPException: + conditional_print(WARNING_QUIT) # Check for rejected recipients -if len(rejected) > 0: - conditional_print(WARNING_REJECTED) - conditional_print('\n'.join(['\t' + email for email in rejected])) - -# Good enough -sys.exit() -# vim: set ft=python noet sts=4 sw=4 ts=4 : +if rejectedRecipients: + conditional_print(WARNING_REJECTED) + conditional_print('\n'.join(['\t' + recipient for recipient in rejectedRecipients])) diff --git a/putmail_dequeue.py b/putmail_dequeue.py index dca0643..76227ce 100755 --- a/putmail_dequeue.py +++ b/putmail_dequeue.py @@ -1,11 +1,11 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# putmail_dequeue.py Read parameters and messages from a queue and pass -# them as arguments and standard input data to putmail.py. +# putmail_dequeue.py Read parameters and messages from a queue and pass +# them as arguments and standard input data to putmail.py. # -# (c) Ricardo García González -# sarbalap-sourceforge _at_ yahoo _dot_ es +# (c) Ricardo García González +# sarbalap-sourceforge _at_ yahoo _dot_ es # # This tiny script is distributed under the X Consortium License. See # LICENSE file for more details. @@ -16,13 +16,13 @@ import os.path import glob import gettext -import cPickle +import pickle ### Initialize ### try: - gettext.install("putmail_dequeue.py") # Always before using _() -except: - pass + gettext.install("putmail_dequeue.py") # Always before using _() +except Exception: + _ = lambda s: s ### Constants ### PUTMAIL_DIR = ".putmail" @@ -32,8 +32,8 @@ ERROR_HOME_UNSET = _("Error: %s environment variable not set") % HOME_EV ### Main program ### -if not os.environ.has_key(HOME_EV): - sys.exit(ERROR_HOME_UNSET) +if not HOME_EV in os.environ: + sys.exit(ERROR_HOME_UNSET) # Get message file names pattern = os.path.join(os.getenv(HOME_EV), PUTMAIL_DIR, QUEUE_SUBDIR, "*") @@ -42,37 +42,35 @@ # Try to send each message (total, sent, deleted) = (0, 0, 0) for msgfn in files: - msgbn = os.path.basename(msgfn) - print _("[%s] Sending message.") % msgbn + msgbn = os.path.basename(msgfn) + print(_("[%s] Sending message.") % msgbn) - try: - (params, text) = cPickle.load(open(msgfn)) + try: + (params, text) = pickle.load(open(msgfn)) - # Launch putmail.py and write the message to its stdin - cmd = "%s %s" % (PUTMAIL_PY, " ".join(params[1:])) - child_stdin = os.popen(cmd, "w") - child_stdin.write(text) + # Launch putmail.py and write the message to its stdin + cmd = "%s %s" % (PUTMAIL_PY, " ".join(params[1:])) + child_stdin = os.popen(cmd, "wb") + child_stdin.write(text) - exit_status = child_stdin.close() - if not exit_status is None: - raise Exception - print _("[%s] Message sent.") % msgbn - sent += 1 + exit_status = child_stdin.close() + if not exit_status is None: + raise Exception() + print(_("[%s] Message sent.") % msgbn) + sent += 1 - try: - print _("[%s] Deleting message file.") % msgbn - os.unlink(msgfn) - print _("[%s] Message deleted.") % msgbn - deleted += 1 + try: + print(_("[%s] Deleting message file.") % msgbn) + os.unlink(msgfn) + print(_("[%s] Message deleted.") % msgbn) + deleted += 1 - except (IOError, OSError): - print _("[%s] Message NOT deleted! Fix queue!") % msgbn + except (IOError, OSError): + print(_("[%s] Message NOT deleted! Fix queue!") % msgbn) - except (IOError, OSError): - print _("[%s] Message NOT sent.") % msgbn + except (IOError, OSError): + print(_("[%s] Message NOT sent.") % msgbn) - total += 1 -# End of for loop + total += 1 -print _("Total: %s\nSent: %s\nDeleted: %s") % (total, sent, deleted) -sys.exit() +print(_("Total: %s\nSent: %s\nDeleted: %s") % (total, sent, deleted)) diff --git a/putmail_enqueue.py b/putmail_enqueue.py index 6267678..f467c2d 100755 --- a/putmail_enqueue.py +++ b/putmail_enqueue.py @@ -1,11 +1,11 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# putmail_enqueue.py Read mail from standard input and save message and -# program arguments to a mail queue. +# putmail_enqueue.py Read mail from standard input and save message and +# program arguments to a mail queue. # -# (c) Ricardo García González -# sarbalap-sourceforge _at_ yahoo _dot_ es +# (c) Ricardo García González +# sarbalap-sourceforge _at_ yahoo _dot_ es # # This tiny script is distributed under the X Consortium License. See # LICENSE file for more details. @@ -16,13 +16,13 @@ import os import os.path import gettext -import cPickle +import pickle ### Initialize ### try: - gettext.install("putmail_enqueue.py") # Always before using _() -except: - pass + gettext.install("putmail_enqueue.py") # Always before using _() +except Exception: + _ = lambda s: s ### Constants ### PUTMAIL_DIR = ".putmail" @@ -34,41 +34,29 @@ ERROR_DATA_OUTPUT = _("Error: unable to write data to queue file") ### Main program ### -if not os.environ.has_key(HOME_EV): - sys.exit(ERROR_HOME_UNSET) +if not HOME_EV in os.environ: + sys.exit(ERROR_HOME_UNSET) queue_dir = os.path.join(os.getenv(HOME_EV), PUTMAIL_DIR, QUEUE_SUBDIR) -# Create message file try: - (msgfd, msgfname) = tempfile.mkstemp("", "", queue_dir) - msgfile = os.fdopen(msgfd, "w") -except (IOError, OSError): - sys.exit(ERROR_CREATE_TEMPFILE) + (msgfd, msgfname) = tempfile.mkstemp("", "", queue_dir) +except OSError: + sys.exit(ERROR_CREATE_TEMPFILE) -# Read parameters and message contents -params = sys.argv try: - message = sys.stdin.read() -except IOError: - msgfile.close() - os.unlink(msgfname) - sys.exit(ERROR_MESSAGE_STDIN) - -# Write data -try: - cPickle.dump((params, message), msgfile) -except IOError: - try: - msgfile.close() - except: - pass - try: - os.unlink(msgfname) - except: - pass - sys.exit(ERROR_DATA_OUTPUT) - -# Close file and exit -msgfile.close() -sys.exit() + with os.fdopen(msgfd, "wb") as msgfile: + try: + message = sys.stdin.read() + except IOError: + sys.exit(ERROR_MESSAGE_STDIN) + try: + pickle.dump((sys.argv, message), msgfile) + except IOError: + sys.exit(ERROR_DATA_OUTPUT) +except SystemExit: + try: + os.unlink(msgfname) + except OSError: + pass + raise