|
|
|
@ -17,46 +17,67 @@ from gi.repository import GLib
|
|
|
|
|
import dbus |
|
|
|
|
import dbus.mainloop.glib |
|
|
|
|
|
|
|
|
|
import os |
|
|
|
|
import re |
|
|
|
|
import tempfile |
|
|
|
|
from aiosmtpd.controller import Controller |
|
|
|
|
from email import parser |
|
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Config: |
|
|
|
|
"""Allow sharing configuration between classes.""" |
|
|
|
|
|
|
|
|
|
def __init__(self): |
|
|
|
|
"""Return the config instance.""" |
|
|
|
|
self.config = configparser.ConfigParser() |
|
|
|
|
self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini") |
|
|
|
|
|
|
|
|
|
def get_config(self): |
|
|
|
|
"""Return the config element. |
|
|
|
|
|
|
|
|
|
:rtype ConfigParser |
|
|
|
|
:return The parsed configuration |
|
|
|
|
""" |
|
|
|
|
return self.config |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MMS2Mail: |
|
|
|
|
""" |
|
|
|
|
The class handling the conversion between MMS and mail format. |
|
|
|
|
|
|
|
|
|
MMS support is provided by python-messaging |
|
|
|
|
Mail support is provided by marrow.mailer |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
def __init__(self, delete=False, force_read=False, |
|
|
|
|
def __init__(self, config, delete=False, force_read=False, |
|
|
|
|
force_unlock=False): |
|
|
|
|
""" |
|
|
|
|
Return class instance. |
|
|
|
|
|
|
|
|
|
:param config: The module configuration file |
|
|
|
|
:type config: ConfigParser |
|
|
|
|
|
|
|
|
|
:param delete: delete MMS after conversion |
|
|
|
|
:type delete: bool |
|
|
|
|
|
|
|
|
|
:param force_read: force converting an already read MMS (batch mode) |
|
|
|
|
:type force_read: bool |
|
|
|
|
|
|
|
|
|
:param disable_dbus: Disable sending dbus commands to mmsd (batch mode) |
|
|
|
|
:type disable_dbus: bool |
|
|
|
|
|
|
|
|
|
:param force_unlock: Force mbox unlocking after a few minutes |
|
|
|
|
:type force_unlock: bool |
|
|
|
|
""" |
|
|
|
|
self.delete = delete |
|
|
|
|
self.force_read = force_read |
|
|
|
|
self.force_unlock = force_unlock |
|
|
|
|
self.config = configparser.ConfigParser() |
|
|
|
|
self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini") |
|
|
|
|
self.attach_mms = self.config.getboolean('mail', 'attach_mms', |
|
|
|
|
fallback=False) |
|
|
|
|
self.domain = self.config.get('mail', 'domain', |
|
|
|
|
fallback=socket.getfqdn()) |
|
|
|
|
self.user = self.config.get('mail', 'user', fallback=getpass.getuser()) |
|
|
|
|
mbox_file = self.config.get('mail', 'mailbox', |
|
|
|
|
fallback=f"/var/mail/{self.user}") |
|
|
|
|
cfg = config.get_config() |
|
|
|
|
self.attach_mms = cfg.getboolean('mail', 'attach_mms', |
|
|
|
|
fallback=False) |
|
|
|
|
self.domain = cfg.get('mail', 'domain', |
|
|
|
|
fallback=socket.getfqdn()) |
|
|
|
|
self.user = cfg.get('mail', 'user', fallback=getpass.getuser()) |
|
|
|
|
mbox_file = cfg.get('mail', 'mailbox', |
|
|
|
|
fallback=f"/var/mail/{self.user}") |
|
|
|
|
self.mailbox = mailbox.mbox(mbox_file) |
|
|
|
|
self.dbus = None |
|
|
|
|
|
|
|
|
@ -178,7 +199,8 @@ class MMS2Mail:
|
|
|
|
|
maintype, subtype = datacontent[0].split('/', 1) |
|
|
|
|
if 'text/plain' in datacontent[0]: |
|
|
|
|
encoding = datacontent[1].get('Charset', 'utf-8') |
|
|
|
|
body += data_part.data.decode(encoding) + '\n' |
|
|
|
|
body += data_part.data.decode(encoding, |
|
|
|
|
errors='replace') + '\n' |
|
|
|
|
continue |
|
|
|
|
extension = str(mimetypes.guess_extension(datacontent[0])) |
|
|
|
|
filename = datacontent[1].get('Name', str(data_id)) |
|
|
|
@ -246,6 +268,103 @@ class MMS2Mail:
|
|
|
|
|
self.dbus.delete_mms(dbus_path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Mail2MMSHandler: |
|
|
|
|
"""The class handling the conversion between mail and MMS format.""" |
|
|
|
|
|
|
|
|
|
def __init__(self, dbusmmsd): |
|
|
|
|
""" |
|
|
|
|
Return the Mail2MMS instance. |
|
|
|
|
|
|
|
|
|
:param dbusmmsd: The DBus MMSd abstraction class |
|
|
|
|
:type dbusmmsd: DbusMMSd() |
|
|
|
|
|
|
|
|
|
:param config: The module configuration file |
|
|
|
|
:type config: ConfigParser |
|
|
|
|
""" |
|
|
|
|
self.parser = parser.BytesParser() |
|
|
|
|
self.pattern = re.compile('^\+[0-9]+$') |
|
|
|
|
self.dbusmmsd = dbusmmsd |
|
|
|
|
mmsd_config = dbusmmsd.get_manager_config() |
|
|
|
|
self.auto_create_smil = mmsd_config.get('AutoCreateSMIL', False) |
|
|
|
|
self.max_attachments = mmsd_config.get('MaxAttachments', 25) |
|
|
|
|
self.total_max_attachment_size = mmsd_config.get( |
|
|
|
|
'TotalMaxAttachmentSize', |
|
|
|
|
1100000) |
|
|
|
|
self.use_delivery_reports = mmsd_config.get('UseDeliveryReports', |
|
|
|
|
False) |
|
|
|
|
|
|
|
|
|
async def handle_DATA(self, server, session, envelope): |
|
|
|
|
""" |
|
|
|
|
Handle the reception of a new mail via smtp. |
|
|
|
|
|
|
|
|
|
:param server: The SMTP server instance |
|
|
|
|
:type server: SMTP |
|
|
|
|
|
|
|
|
|
:param session: The session instance currently being handled |
|
|
|
|
:type session: Session |
|
|
|
|
|
|
|
|
|
:param envelope: The envelope instance of the current SMTP Transaction |
|
|
|
|
:type envelope: Envelope |
|
|
|
|
""" |
|
|
|
|
recipients = [] |
|
|
|
|
attachments = [] |
|
|
|
|
smil = None |
|
|
|
|
for r in envelope.rcpt_tos: |
|
|
|
|
number = r.split('@')[0] |
|
|
|
|
if self.pattern.search(number): |
|
|
|
|
log.debug(f'Add recipient number : {number}') |
|
|
|
|
recipients.append(number) |
|
|
|
|
else: |
|
|
|
|
log.debug(f'Ignoring recipient : {r}') |
|
|
|
|
if len(recipients) == 0: |
|
|
|
|
log.info('No sms recipient') |
|
|
|
|
return '553 Requested action not taken: mailbox name not allowed' |
|
|
|
|
|
|
|
|
|
mail = self.parser.parsebytes(envelope.content) |
|
|
|
|
cid = 1 |
|
|
|
|
total_size = 0 |
|
|
|
|
with tempfile.TemporaryDirectory(prefix='mailtomms-') as tmp_dir: |
|
|
|
|
for part in mail.walk(): |
|
|
|
|
content_type = part.get_content_type() |
|
|
|
|
if 'multipart' in content_type: |
|
|
|
|
continue |
|
|
|
|
filename = part.get_filename() |
|
|
|
|
if not filename: |
|
|
|
|
ext = mimetypes.guess_extension(part.get_content_type()) |
|
|
|
|
if not ext: |
|
|
|
|
# Use a generic bag-of-bits extension |
|
|
|
|
ext = '.bin' |
|
|
|
|
filename = f'part-{cid:03d}{ext}' |
|
|
|
|
if filename == 'smil.xml': |
|
|
|
|
smil = part.get_payload(decode=True) |
|
|
|
|
continue |
|
|
|
|
path = os.path.join(tmp_dir, filename) |
|
|
|
|
if content_type == 'text/plain': |
|
|
|
|
with open(path, 'wt', encoding='utf-8') as af: |
|
|
|
|
charset = part.get_content_charset(failobj='utf-8') |
|
|
|
|
total_size += af.write(part. |
|
|
|
|
get_payload(decode=True). |
|
|
|
|
decode(charset)) |
|
|
|
|
else: |
|
|
|
|
with open(path, 'wb') as af: |
|
|
|
|
total_size += af.write(part. |
|
|
|
|
get_payload(decode=True)) |
|
|
|
|
attachments.append((f"cid-{cid}", content_type, path)) |
|
|
|
|
cid += 1 |
|
|
|
|
if len(attachments) == 0: |
|
|
|
|
return '550 No attachments found' |
|
|
|
|
elif len(attachments) > self.max_attachments: |
|
|
|
|
return '550 Too much attachments' |
|
|
|
|
elif total_size > self.total_max_attachment_size: |
|
|
|
|
return '554 5.3.4 Message too big for system' |
|
|
|
|
try: |
|
|
|
|
self.dbusmmsd.send_mms(recipients, attachments, smil) |
|
|
|
|
except dbus.exceptions.DBusException as e: |
|
|
|
|
log.error(e) |
|
|
|
|
return '421 mmsd service not available' |
|
|
|
|
return '250 OK' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DbusMMSd(): |
|
|
|
|
"""Use DBus communication with mmsd.""" |
|
|
|
|
|
|
|
|
@ -297,6 +416,69 @@ class DbusMMSd():
|
|
|
|
|
log.debug(f"Deleting MMS {dbus_path}") |
|
|
|
|
message.Delete() |
|
|
|
|
|
|
|
|
|
def get_service(self): |
|
|
|
|
""" |
|
|
|
|
Get mmsd Service Interface. |
|
|
|
|
|
|
|
|
|
:return the mmsd service |
|
|
|
|
:rtype dbus.Interface |
|
|
|
|
""" |
|
|
|
|
manager = dbus.Interface(self.bus.get_object('org.ofono.mms', |
|
|
|
|
'/org/ofono/mms'), |
|
|
|
|
'org.ofono.mms.Manager') |
|
|
|
|
services = manager.GetServices() |
|
|
|
|
path = services[0][0] |
|
|
|
|
service = dbus.Interface(self.bus.get_object('org.ofono.mms', path), |
|
|
|
|
'org.ofono.mms.Service') |
|
|
|
|
return service |
|
|
|
|
|
|
|
|
|
def get_manager_config(self): |
|
|
|
|
""" |
|
|
|
|
Ask mmsd its properties. |
|
|
|
|
|
|
|
|
|
:return the mmsd manager service properties |
|
|
|
|
:rtype dict |
|
|
|
|
""" |
|
|
|
|
service = self.get_service() |
|
|
|
|
return service.GetProperties() |
|
|
|
|
|
|
|
|
|
def send_mms(self, recipients, attachments, smil=None): |
|
|
|
|
""" |
|
|
|
|
Ask mmsd to send a MMS. |
|
|
|
|
|
|
|
|
|
:param recipients: The mms recipients phone numbers |
|
|
|
|
:type recipients: Array(str) |
|
|
|
|
|
|
|
|
|
:param attachments: The mms attachments [name, mime type, filepath] |
|
|
|
|
:type attachments: Array(str,str,str) |
|
|
|
|
|
|
|
|
|
:param smil: The Smil.xml content allowing MMS customization |
|
|
|
|
:type smil: str |
|
|
|
|
""" |
|
|
|
|
service = self.get_service() |
|
|
|
|
|
|
|
|
|
mms_recipients = dbus.Array([], signature=dbus.Signature('s')) |
|
|
|
|
for r in recipients: |
|
|
|
|
mms_recipients.append(dbus.String(r)) |
|
|
|
|
|
|
|
|
|
if smil: |
|
|
|
|
log.debug("Send MMS as Related") |
|
|
|
|
mms_smil = dbus.String(smil) |
|
|
|
|
else: |
|
|
|
|
log.debug("Send MMS as Mixed") |
|
|
|
|
mms_smil = "" |
|
|
|
|
|
|
|
|
|
mms_attachments = dbus.Array([], signature=dbus.Signature('(sss)')) |
|
|
|
|
for a in attachments: |
|
|
|
|
log.debug("Attachment: ({})".format(a)) |
|
|
|
|
mms_attachments.append(dbus.Struct((dbus.String(a[0]), |
|
|
|
|
dbus.String(a[1]), |
|
|
|
|
dbus.String(a[2]) |
|
|
|
|
), signature=None)) |
|
|
|
|
|
|
|
|
|
path = service.SendMessage(mms_recipients, mms_smil, mms_attachments) |
|
|
|
|
log.debug(path) |
|
|
|
|
|
|
|
|
|
def add_signal_receiver(self): |
|
|
|
|
"""Add a signal receiver to the current bus.""" |
|
|
|
|
if self.mms2mail: |
|
|
|
@ -326,10 +508,15 @@ def main():
|
|
|
|
|
parser = argparse.ArgumentParser() |
|
|
|
|
mode = parser.add_mutually_exclusive_group() |
|
|
|
|
mode.add_argument("-d", "--daemon", |
|
|
|
|
help="Use dbus signal from mmsd to trigger conversion", |
|
|
|
|
action='store_true', dest='watcher') |
|
|
|
|
help="start in daemon mode ", |
|
|
|
|
action='store_true', dest='daemon') |
|
|
|
|
mode.add_argument("-f", "--file", nargs='+', |
|
|
|
|
help="Parse specified mms files and quit", dest='files') |
|
|
|
|
help="Start in batch mode, parse specified mms files", |
|
|
|
|
dest='files') |
|
|
|
|
parser.add_argument('--disable-smtp', action='store_true', |
|
|
|
|
dest='disable_smtp') |
|
|
|
|
parser.add_argument('--disable-mms-delivery', action='store_true', |
|
|
|
|
dest='disable_mms_delivery') |
|
|
|
|
parser.add_argument('--delete', action='store_true', dest='delete', |
|
|
|
|
help="Ask mmsd to delete the converted MMS") |
|
|
|
|
parser.add_argument('--force-read', action='store_true', |
|
|
|
@ -356,8 +543,20 @@ def main():
|
|
|
|
|
ch.setFormatter(formatter) |
|
|
|
|
log.addHandler(ch) |
|
|
|
|
|
|
|
|
|
c = Config() |
|
|
|
|
d = DbusMMSd() |
|
|
|
|
m = MMS2Mail(delete=args.delete, force_read=args.force_read, |
|
|
|
|
|
|
|
|
|
h = Mail2MMSHandler(dbusmmsd=d) |
|
|
|
|
|
|
|
|
|
controller = Controller(h, |
|
|
|
|
hostname=c.get_config().get('smtp', 'hostname', |
|
|
|
|
fallback='localhost'), |
|
|
|
|
port=c.get_config().get('smtp', 'port', |
|
|
|
|
fallback=2525)) |
|
|
|
|
|
|
|
|
|
m = MMS2Mail(config=c, |
|
|
|
|
delete=args.delete, |
|
|
|
|
force_read=args.force_read, |
|
|
|
|
force_unlock=args.force_unlock) |
|
|
|
|
m.set_dbus(d) |
|
|
|
|
|
|
|
|
@ -365,13 +564,21 @@ def main():
|
|
|
|
|
for mms_file in args.files: |
|
|
|
|
m.convert(path=mms_file) |
|
|
|
|
return |
|
|
|
|
elif args.watcher: |
|
|
|
|
elif args.daemon: |
|
|
|
|
log.info("Starting mms2mail in daemon mode") |
|
|
|
|
d.set_mms2mail(m) |
|
|
|
|
d.add_signal_receiver() |
|
|
|
|
if not args.disable_smtp: |
|
|
|
|
log.info("Activating smtp to mmsd server") |
|
|
|
|
controller.start() |
|
|
|
|
if not args.disable_mms_delivery: |
|
|
|
|
log.info("Activating mms to mbox server") |
|
|
|
|
d.set_mms2mail(m) |
|
|
|
|
d.add_signal_receiver() |
|
|
|
|
else: |
|
|
|
|
parser.print_help() |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
d.run() |
|
|
|
|
controller.stop() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
|
|
|