#!/usr/bin/python # -*- coding: utf-8 -*- # # Mail2SMS # # Postfix transport to sending SMS by using SMS Gateway Android App # # SMS recipients are detected in call arguments and To, CC and BCC # mail headers. The phone number detection mechamism look for a suite # of 10 digits or more or an email with user part is a suite of 10 # digits or more. # # The SMS content is the concatenation of the mail's subject with all # text/plain parts of the mail's body. If no text/plain part is found, # other parts of the email will be concatenated and HTML parts of the # email will be convert in full text. # # Usage : # # in master.cf : # # sms unix - n n - 1 pipe # flags=Rq user=nobody:nogroup argv=/usr/local/sbin/mail2sms ${user} # # in transport : # # sms.example.tld sms # # Dependencies (on Debian system) : # * Python (python2.7 and python2.7-minimal Debian packages) # * Python html2text module in python-html2text Debian package # # Android App : # * Install this app : # https://play.google.com/store/apps/details?id=eu.apksoft.android.smsgateway # * In App # * In Settings screen : # * check "Listen for HTTP send SMS commands" # * check "Prevent CPU sleep mode" (adviced) # * check "Start gateway automatically after phone boot" (adviced) # * Start server in main screen # # Usage : # # mail2sms [phone number] # Mail content is sent through STDIN # # Author: Benjamin Renard # # Copyright (C) 2017, Benjamin Renard # # Licensed under the GNU General Public License version 3 or # any later version. # See the LICENSE file for a full license statement. # import sys import os import email import html2text import logging import re from optparse import OptionParser from datetime import datetime import email.header from email.MIMEText import MIMEText from email.MIMEMultipart import MIMEMultipart import urllib2 try: import urlparse from urllib import urlencode except: # For Python 3 import urllib.parse as urlparse from urllib.parse import urlencode ################# # Configuration # ################# default_sms_gw_host='smsgw.example.fr' default_sms_gw_port=9090 default_sms_gw_pwd=None default_sms_gw_timeout=10 ############# # CONSTANTS # ############# EX_TEMPFAIL=75 EX_SOFTWARE=70 EX_UNAVAILABLE=69 EX_DATAERR=65 EX_OK=0 ############# # Functions # ############# def decode(text, prefer_encoding=None): try_enc=['utf-8', 'iso-8859-15'] if prefer_encoding: try_enc.insert(0,prefer_encoding) for i in try_enc: try: return unicode(text.decode(i)) except BaseException, e: continue return unicode(text.decode('utf-8', errors = 'replace')) def is_phone_number(txt): if re.match('^[0-9]{10,}$', txt): return True return False def mail_address_to_phone_number(txt): m=re.match('^([0-9]{10,})@.*$', txt) if m: return m.group(1) m=re.match('^.* <([0-9]{10,})@.*>$', txt) if m: return m.group(1) return False def send_sms(recipient, text): if len(text) > 160 and options.split: logging.debug('Text contain more than 160 caracteres : split in multiple SMS') start=0 while True: if start > len(text)-1: break if start==0: stop=start+157 msg=text[start:stop]+u'...' else: stop=start+154 if stop>=(len(text)-1): msg=u'...'+text[start:] else: msg=u'...'+text[start:stop]+u'...' if not send_sms(recipient, msg): return False start=stop return True url_params={ 'phone': recipient, 'text': text.encode('utf8') } if options.smspwd: url_params['password']=options.smspwd url_parts=[ 'http', '%s:%s' % (options.smshost, options.smsport), '/sendsms', '', urlencode(url_params), '' ] url=urlparse.urlunparse(url_parts) logging.debug(u'Send SMS using url : %s' % url) try: request=urllib2.urlopen(url, timeout=options.smstimeout) data=request.read() except Exception,e: logging.fatal('Fail to open URL %s : %s' % (url,e)) return False logging.debug(u'SMS gateway return : "%s"' % data) if re.search('Mess?age SENT!', data): return True logging.error('Fail to send SMS. Gateway return : "%s"' % data) return False def check_gateway_status(): url='http://%s:%s' % (options.smshost, options.smsport) try: request=urllib2.urlopen(url, timeout=options.smstimeout) data=request.read() except Exception,e: logging.fatal('Fail to open URL %s : %s' % (url,e)) return False logging.debug(u'SMS gateway return : "%s"' % data) if re.search('Welcome to SMS Gateway', data): return True return False ###### # DO # ###### parser = OptionParser() parser.add_option('-j', '--just-try', action="store_true", dest="justtry", help="Enable just-try mode") parser.add_option('-J', '--just-one', action="store_true", dest="justone", help="Enable just-one mode") parser.add_option('-v', '--verbose', action="store_true", dest="verbose", help="Enable verbose mode") parser.add_option('-d', '--debug', action="store_true", dest="debug", help="Enable debug mode") parser.add_option('-l', '--log-file', action="store", type="string", dest="logfile", help="Log file path") parser.add_option('-s', '--auto-split', action="store_true", dest="split", help="Auto split long SMS by small SMS of 160 characters") parser.add_option('-b', '--backup-mail', action="store", type="string", dest="bkpdir", help="Backup mail receive in specified directory") parser.add_option('-H', '--sms-host', action="store", type="string", dest="smshost", help="SMS gateway host (Default : %s)" % default_sms_gw_host, default=default_sms_gw_host) parser.add_option('-p', '--sms-port', action="store", type="int", dest="smsport", help="SMS gateway port (Default : %s)" % default_sms_gw_port, default=default_sms_gw_port) parser.add_option('-P', '--sms-password', action="store", type="string", dest="smspwd", help="SMS gateway password", default=default_sms_gw_pwd) parser.add_option('-t', '--sms-timeout', action="store", type="int", dest="smstimeout", help="SMS gateway timeout (Default : %s)" % default_sms_gw_timeout, default=default_sms_gw_timeout) parser.add_option('-c', '--check', action="store_true", dest="check", help="Enable check SMS gateway mode") (options, args) = parser.parse_args() logformat = '%(asctime)s - Mail to SMS - %(levelname)s - %(message)s' if options.debug: loglevel = logging.DEBUG elif options.verbose: loglevel = logging.INFO else: loglevel = logging.WARNING if options.logfile: logging.basicConfig(filename=options.logfile,level=loglevel,format=logformat) else: logging.basicConfig(level=loglevel,format=logformat) if options.check: if check_gateway_status(): print "OK - SMS gateway is reponding" sys.exit(0) print "CRITICAL - SMS gateway not reponding" sys.exit(2) logging.info('Read mail from stdin') mail_input="" count=0 for line in sys.stdin: mail_input+=line count+=1 logging.info('%s lines readed from stdin' % count) logging.info('Convert mail as email object') mail=email.message_from_string(mail_input) mailfrom=mail.get('From') if not mailfrom: mailfrom='Unknow sender' maildate=datetime.now() now_ldap=maildate.strftime(u'%Y%m%d%H%M%SZ') if options.bkpdir: bkpfile="%s/%s.mail" % (options.bkpdir,maildate.strftime('%Y%m%d%H%M%S')) try: fd=open(bkpfile, 'w') fd.write("Receive from %s (Options : %s)\n" % (mailfrom,options)) fd.write("=============================================================\n") fd.write(mail_input) fd.close() except BaseException, e: logging.error('Fail to backup input mail in %s file : %s' % (bkpfile,e)) logging.info('Mail from %s' % mailfrom) sms_recipients=[] if len(args) > 0: for arg in args: if is_phone_number(arg): if arg not in sms_recipients: sms_recipients.append(arg) else: arg=mail_address_to_phone_number(arg) if arg and arg not in sms_recipients: sms_recipients.append(arg) for header in ('To', 'CC', 'BCC'): raw_mail_to=mail.get(header , None) if isinstance(raw_mail_to, str): mail_tos=email.header.decode_header(raw_mail_to) logging.debug('Mail header %s : "%s"' % (header, mail_tos)) if isinstance(mail_tos, list): for (to, encoding) in mail_tos: to=decode(to, encoding) to=mail_address_to_phone_number(to) if to and to not in sms_recipients: sms_recipients.append(to) if len(sms_recipients)==0: logging.warning('No SMS recipient found') sys.exit(EX_DATAERR) logging.info('SMS recipient(s) : %s' % ', '.join(sms_recipients)) sms_content="" raw_mail_subject=mail.get('Subject', None) if isinstance(raw_mail_subject, str): mail_subject=u"" mail_subjects=email.header.decode_header(raw_mail_subject) if isinstance(mail_subjects, list): for (subject, encoding) in mail_subjects: subject=decode(subject, encoding) mail_subject+=subject logging.debug(u'Mail subject : "%s"' % mail_subject) sms_content=mail_subject+u"\n" else: logging.warning('Fail to decode email subject : "%s"' % raw_mail_subject) else: logging.warning('No subject in this email found') logging.info('Extract mail text parts') count=0 mail_content={} h = html2text.HTML2Text() h.ignore_links = True for part in mail.walk(): if part.get_content_maintype() == 'text': type=part.get_content_type() if type not in mail_content: mail_content[type]=[] text=decode(part.get_payload(decode=True), prefer_encoding=part.get_content_charset()) if part.get_content_type() == 'text/html': text=h.handle(text) mail_content[type].append(unicode(text)) count+=1 logging.info('Found %s text parts in email' % count) mail_text="" if 'text/plain' in mail_content: logging.info('%s text/plain part founded. Concatenate all and use it as input' % len(mail_content['text/plain'])) for text in mail_content['text/plain']: mail_text+="%s\n" % text if mail_text=="": logging.info('No text/plain or text/csv part founded. Concatenate all other text part and use it as input') for type in mail_content: for text in mail_content[type]: mail_text+="%s\n" % text sms_content+=mail_text if not sms_content: logging.debug('No SMS content detect in this email. Stop') sys.exit(EX_OK) logging.info('Send SMS') for recipient in sms_recipients: logging.debug('Send SMS to %s' % recipient) if not send_sms(recipient, sms_content): logging.fatal('Fail to send SMS to %s. Exit with TEMPFAIL code.' % recipient) sys.exit(EX_TEMPFAIL) logging.info('SMS sent to %s' % recipient) sys.exit(EX_OK)