2017-10-24 17:18:58 +02:00
|
|
|
#!/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):
|
2017-10-27 15:05:58 +02:00
|
|
|
try_enc=['utf-8', 'iso-8859-15']
|
2017-10-24 17:18:58 +02:00
|
|
|
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):
|
2017-11-14 17:28:48 +01:00
|
|
|
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
|
2017-10-24 17:18:58 +02:00
|
|
|
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)
|
2017-11-14 17:28:48 +01:00
|
|
|
logging.debug(u'Send SMS using url : %s' % url)
|
2017-10-24 17:18:58 +02:00
|
|
|
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")
|
|
|
|
|
2017-11-14 17:28:48 +01:00
|
|
|
parser.add_option('-s',
|
|
|
|
'--auto-split',
|
|
|
|
action="store_true",
|
|
|
|
dest="split",
|
|
|
|
help="Auto split long SMS by small SMS of 160 characters")
|
|
|
|
|
2017-10-24 17:18:58 +02:00
|
|
|
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:
|
2017-10-27 15:05:58 +02:00
|
|
|
to=decode(to, encoding)
|
2017-10-24 17:18:58 +02:00
|
|
|
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:
|
2017-10-27 15:05:58 +02:00
|
|
|
subject=decode(subject, encoding)
|
2017-10-24 17:18:58 +02:00
|
|
|
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)
|