mail2sms/mail2sms

417 lines
11 KiB
Python
Executable File

#!/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 <brenard@zionetrix.net>
#
# 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)