diff --git a/.gitignore b/.gitignore index 13d1490..2f27ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,5 @@ dmypy.json # Pyre type checker .pyre/ +#VScode +.vscode/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ff7038b --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +install: + mkdir -p ${HOME}/.local/bin + mkdir -p ${HOME}/.config/systemd/user + install -m 700 ./mms2mail ${HOME}/.local/bin/ + install -m 755 ./mms2mail.service ${HOME}/.config/systemd/user/ + systemctl --user daemon-reload + +configure: + systemctl --user enable mms2mail + +start: + systemctl --user start mms2mail + +deb-deps: + sudo apt install python3-pydbus python3-aiosmtpd + +pypy-deps: + pip install --user -r requirements.txt diff --git a/README.md b/README.md index 7e32662..ae8b3a1 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,53 @@ # mms2mail -Convert MMSd MMS file to mbox. +Mail bridge for mmsd. + +mms2mail: +* convert incoming mms from mmsd to mail and store it in unix mbox. +* provide a smtp server converting mail to mms with mmsd. + +By default: +* store mails in the current user mbox (```/var/mail/$USER```) + * in case the mbox is locked by another process the output could be found in ```$HOME/.mms/failsafembox``` +* listen on localhost port 2525 for mail ## installation ### dependency - python3 - - python3-watchdog (pip install watchdog) - - python-messaging (pip install python-messaging) - - marrow.mailer (pip install marrow.mailer) + - python3-aiosmtpd + - python3-pydbus ### setup -Install the dependency and mms2mail: + +Install the dependency and mms2mail (on debian based distribution): ``` -pip install --user watchdog # or sudo apt install python3-watchdog -pip install --user marrow-mailer -pip install --user python-messaging +make deb-deps install +``` +For other distribution: +``` +make pypy-deps install +``` + +To enable the daemon mode in systemd user : +``` +make configure start +``` + +Depending on your distribution, you might have to add your account to the ```mail``` group to be able to lock and use the system mbox. +On Debian based distribution : +``` +sudo addgroup $(whoami) mail +``` + +#### manual install + +Install the dependency and mms2mail (on debian based distribution): +``` +sudo apt-get install python3 +sudo apt-get install python3-pydbus +sudo apt-get install python3-aiosmtpd mkdir -p ~/.local/bin cp mms2mail ~/.local/bin @@ -29,6 +60,12 @@ systemctl --user daemon-reload systemctl --user enable mms2mail systemctl --user start mms2mail ``` + +Depending on your distribution, you might have to add your account to the ```mail``` group to be able to lock and use the system mbox. +On Debian based distribution : +``` +sudo addgroup $(whoami) mail +``` ### config An optional configuration file can be put in the home folder : ```$HOME/.mms/modemmanager/mms2mail.ini```. The default value are : @@ -39,19 +76,40 @@ mailbox = /var/mail/$USER ; the mailbox where mms are appended user = $USER ; the user account specified as recipient domain = $HOSTNAME ; the domain part appended to phone number and user attach_mms = false ; whether to attach the full mms binary file +delete_from_mmsd = false ; delete mms from mmsd storage upon successful conversion + +[smtp] +hostname = localhost +port = 2525 ``` ## usage +### reference ``` -mms2mail [-h] [-d [{dbus,filesystem}] | -f FILES [FILES ...]] [--delete] [--force-read] +mms2mail [-h] [--disable-smtp] [--disable-mms-delivery] [--force-read] [--force-unlock] [-l {critical,error,warning,info,debug}] optional arguments: -h, --help show this help message and exit - -d [{dbus,filesystem}], --daemon [{dbus,filesystem}] - Use dbus signal from mmsd by default but can also watch mmsd storage folder (useful for mmsd < 1.0) - -f FILES [FILES ...], --file FILES [FILES ...] - Parse specified mms files and quit - --delete Ask mmsd to delete the converted MMS + --disable-smtp + --disable-mms-delivery --force-read Force conversion even if MMS is marked as read -``` \ No newline at end of file + --force-unlock BEWARE COULD LEAD TO WHOLE MBOX CORRUPTION Force unlocking the mbox after a few minutes /!\ + -l {critical,error,warning,info,debug}, --logging {critical,error,warning,info,debug} + Define the logger output level +``` + +### Sending MMS + +To send MMS, mail address not in the following format would be ignored : +```+123456789@domain``` with phone number in international format. + +#### with Mutt : +To be able to send mms with mutt you need it to be built with SMTP support. +And and the following line in your ```$HOME/.muttrc```: +``` +set smtp_url = "smtp://localhost:2525" +set ssl_starttls = no +set ssl_force_tls = no + +``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..dae34e2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,9 @@ +# mms2mail + +## To Do + +* HTML message + * mms2mail : add as HTML body, currently added as attachments + * mail2mms : convert to text plain in case of html only mail + +* A lot of other things... \ No newline at end of file diff --git a/mms2mail b/mms2mail index 69c35f6..cc0793c 100755 --- a/mms2mail +++ b/mms2mail @@ -1,45 +1,45 @@ #!/usr/bin/python3 """An mms to mail converter for mmsd.""" -# upstream bug dirty fix -# https://github.com/marrow/mailer/issues/87#issuecomment-689586587 -import sys -if sys.version_info[0] == 3 and sys.version_info[1] > 7: - sys.modules["cgi.parse_qsl"] = None -# upstream bug dirty fix -# https://github.com/marrow/mailer/issues/87#issuecomment-713319548 -import base64 -if sys.version_info[0] == 3 and sys.version_info[1] > 8: - def encodestring(value): - """ - Encode string in base64. - - :param value: the string to encode. - :type value: str - - :rtype str - :return: the base64 encoded string - """ - return base64.b64encode(value) - base64.encodestring = encodestring -# end bugfix - import argparse import configparser -import re -import time import getpass import socket import mimetypes +import time +import logging from pathlib import Path -from messaging.mms.message import MMSMessage -from marrow.mailer import Mailer, Message +import mailbox +import email from gi.repository import GLib -import dbus -import dbus.mainloop.glib -from watchdog.observers import Observer -from watchdog.events import FileSystemEventHandler +from pydbus import SessionBus +from datetime import datetime + +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: @@ -47,44 +47,355 @@ 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, force_read=False, + force_unlock=False): """ Return class instance. - :param delete: delete MMS after conversion - :type delete: bool + :param config: The module configuration file + :type config: ConfigParser :param force_read: force converting an already read MMS (batch mode) :type force_read: bool - """ - self.delete = delete - self.force_read = force_read - 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}") - self.mailer = Mailer({'manager.use': 'immediate', - 'transport.use': 'mbox', - 'transport.file': mbox_file}) - dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - self.bus = dbus.SessionBus() - def get_bus(self): + :param force_unlock: Force mbox unlocking after a few minutes + :type force_unlock: bool + """ + self.force_read = force_read + self.force_unlock = force_unlock + cfg = config.get_config() + self.attach_mms = cfg.getboolean('mail', 'attach_mms', + fallback=False) + self.delete = cfg.getboolean('mail', 'delete_from_mmsd', + 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 + + def set_dbus(self, dbusmmsd): """ Return the DBus SessionBus. - :rtype dbus.SessionBus() - :return: an active SessionBus + :param dbusmmsd: The DBus MMSd abstraction class + :type dbusmmsd: DbusMMSd() """ - return self.bus + self.dbus = dbusmmsd + + def check_mms(self, path, properties): + """ + Check wether the provided file would be converted. + + :param path: the mms filesystem path + :type path: str + + :param properties: the mms properties + :type properties: Array + + :return the mms status or None + :rtype str + """ + # Check for mmsd data file + if not Path(f"{path}").is_file(): + log.error("MMS file not found : aborting") + return None + # Check for mmsd status file + status = configparser.ConfigParser() + if not Path(f"{path}.status").is_file(): + log.error("MMS status file not found : aborting") + return None + status.read_file(open(f"{path}.status")) + if not (self.force_read or not status.getboolean('info', 'read')): + log.error("Already converted MMS : aborting") + return None + return status['info']['state'] + + def message_added(self, name, value): + """Trigger conversion on MessageAdded signal.""" + if value['Status'] == 'downloaded' or value['Status'] == 'received': + log.debug(f"New incoming MMS found ({name.split('/')[-1]})") + self.convert(path=value['Attachments'][0][2], dbus_path=name, + properties=value) + else: + log.debug(f"New outgoing MMS found ({name.split('/')[-1]})") + + def convert(self, path, dbus_path, properties): + """ + Convert a provided mms file to a mail stored in a mbox. + + :param path: the mms filesystem path + :type path: str + + :param dbus_path: the mms dbus path + :type dbus_path: str + + :param properties: the mms properties + :type properties: Array + """ + # Check if the provided file present + status = self.check_mms(path, properties) + if not status: + log.error("MMS file not convertible.") + return + + message = email.message.EmailMessage() + + # Generate Mail Headers + mms_from = properties.get('Sender', "unknown") + log.debug(f"MMS[From]: {mms_from}") + if '@' in mms_from: + message['From'] = mms_from + else: + message['From'] = f"{mms_from}@{self.domain}" + + to = properties.get('Modem Number', None) + if to: + message['To'] = f"{mms_from}@{self.domain}" + recipients = "" + for r in properties['Recipients']: + if to and to in r: + continue + log.debug(f'MMS[CC] : {r}') + if '@' in r: + recipients += f"{r}," + else: + recipients += f"{r}@{self.domain}," + if recipients: + recipients = recipients[:-1] + if to: + message['CC'] = recipients + else: + message['To'] = recipients + + message['Subject'] = properties.get('Subject', + f"MMS from {mms_from}") + mms_date = properties.get('Date') + if mms_date: + mms_datetime = datetime.strptime(mms_date, '%Y-%m-%dT%H:%M:%S%z') + mail_date = email.utils.format_datetime(mms_datetime) + message['Date'] = mail_date or email.utils.formatdate() + + message.preamble = "This mail is converted from a MMS." + body = "" + attachments = [] + for attachment in properties['Attachments']: + cid = attachment[0] + mimetype = attachment[1] + contentfile = attachment[2] + offset = attachment[3] + size = attachment[4] + with open(contentfile, 'rb') as f: + f.seek(offset, 0) + content = f.read(size) + if mimetype is not None: + if 'text/plain' in mimetype: + mimetype, charset = mimetype.split(';', 1) + encoding = charset.split('=')[1] + body += content.decode(encoding, + errors='replace') + '\n' + continue + maintype, subtype = mimetype.split('/', 1) + extension = str(mimetypes.guess_extension(mimetype)) + filename = cid + attachments.append([content, maintype, + subtype, filename + extension]) + if body: + message.set_content(body) + for a in attachments: + message.add_attachment(a[0], + maintype=a[1], + subtype=a[2], + filename=a[3]) + + # Add MMS binary file, for debugging purpose + # or reparsing in the future + if self.attach_mms: + with open(path, 'rb') as fp: + message.add_attachment(fp.read(), + maintype='application', + subtype='octet-stream', + filename=path.split('/')[-1]) + + # Write the mail in case of mbox lock retry for 5 minutes + # Ultimately write in an mbox in the home folder + end_time = time.time() + (5 * 60) + while True: + try: + # self.mailer.send(message) + self.mailbox.lock() + self.mailbox.add(mailbox.mboxMessage(message)) + self.mailbox.flush() + self.mailbox.unlock() + break + except (mailbox.ExternalClashError, FileExistsError) as e: + log.warn(f"Exception Mbox lock : {e}") + if time.time() > end_time: + if self.force_unlock: + log.error("Force removing lock") + self.mailbox.unlock() + else: + fs_mbox_path = f"{Path.home()}/.mms/failsafembox" + fs_mbox = mailbox.mbox(fs_mbox_path) + log.warning(f"Writing in internal mbox {fs_mbox_path}") + try: + fs_mbox.unlock() + fs_mbox.lock() + fs_mbox.add(mailbox.mboxMessage(str(message))) + fs_mbox.flush() + fs_mbox.unlock() + break + except (mailbox.ExternalClashError, + FileExistsError) as e: + log.error(f"Failsafe Mbox error : {e}") + log.error(f"MMS cannot be written to any mbox : \ + {path.split('/')[-1]}") + finally: + break + + else: + time.sleep(5) + # Ask mmsd to mark message as read and delete it + if properties: + self.dbus.mark_mms_read(dbus_path) + if self.delete: + self.dbus.delete_mms(dbus_path) + + def convert_stored_mms(self): + """Convert all mms from mmsd storage.""" + log.info('INIT : Converting MMs from storage') + messages = self.dbus.get_messages() + for m in messages: + self.message_added(name=m[0], value=m[1]) + + +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) + subject = mail.get('subject', failobj=None) + 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=recipients, + attachments=attachments, + subject=subject, + smil=smil) + except Exception as e: + log.error(e) + return '421 mmsd service not available' + return '250 OK' + + +class DbusMMSd(): + """Use DBus communication with mmsd.""" + + def __init__(self, mms2mail=None): + """ + Return a DBusWatcher instance. + + :param mms2mail: An mms2mail instance to convert new mms + :type mms2mail: mms2mail() + """ + self.mms2mail = mms2mail + self.bus = SessionBus() + + def set_mms2mail(self, mms2mail): + """ + Set mms2mail instance handling dbus event. + + :param mms2mail: An mms2mail instance to convert new mms + :type mms2mail: mms2mail() + """ + self.mms2mail = mms2mail def mark_mms_read(self, dbus_path): """ @@ -93,10 +404,8 @@ class MMS2Mail: :param dbus_path: the mms dbus path :type dbus_path: str """ - message = dbus.Interface(self.bus.get_object('org.ofono.mms', - dbus_path), - 'org.ofono.mms.Message') - print(f"Marking MMS as read {dbus_path}", file=sys.stderr) + message = self.bus.get('org.ofono.mms', dbus_path) + log.debug(f"Marking MMS as read {dbus_path}") message.MarkRead() def delete_mms(self, dbus_path): @@ -106,229 +415,183 @@ class MMS2Mail: :param dbus_path: the mms dbus path :type dbus_path: str """ - message = dbus.Interface(self.bus.get_object('org.ofono.mms', - dbus_path), - 'org.ofono.mms.Message') - print(f"Deleting MMS {dbus_path}", file=sys.stderr) + message = self.bus.get('org.ofono.mms', dbus_path) + log.debug(f"Deleting MMS {dbus_path}") message.Delete() - def check_mms(self, path): + def get_service(self): """ - Check wether the provided file would be converted. + Get mmsd Service Interface. - :param path: the mms filesystem path - :type path: str + :return the mmsd service + :rtype dbus.Interface + """ + manager = self.bus.get('org.ofono.mms', '/org/ofono/mms') + services = manager.GetServices() + path = services[0][0] + service = self.bus.get('org.ofono.mms', path) + return service + + def get_messages(self): + """ + Ask mmsd all stored mms. + + :return all mms from mmsd storage + :rtype Array + """ + service = self.get_service() + return service.GetMessages() + + 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 get_send_message_version(self): + """ + Ask mmsd its SendMessage method Signature. + + :return true if mmsd is mmsd-tng allowing Subject in mms :rtype bool """ - # Check for mmsd data file - if not Path(f"{path}").is_file(): - print("MMS file not found : aborting", file=sys.stderr) - return False - # Check for mmsd status file - status = configparser.ConfigParser() - if not Path(f"{path}.status").is_file(): - print("MMS status file not found : aborting", file=sys.stderr) - return False - status.read_file(open(f"{path}.status")) - # Allow only incoming MMS for the time beeing - if not (status['info']['state'] == 'downloaded' or - status['info']['state'] == 'received'): - print("Outgoing MMS : aborting", file=sys.stderr) - return False - if not (self.force_read or not status.getboolean('info', 'read')): - print("Already converted MMS : aborting", file=sys.stderr) - return False - return True + if not hasattr(self, 'mmsdtng'): + from xml.dom import minidom + mmsdtng = False + svc = self.get_service() + i = svc.Introspect() + dom = minidom.parseString(i) + for method in dom.getElementsByTagName('method'): + if method.getAttribute('name') == "SendMessage": + for arg in method.getElementsByTagName('arg'): + if arg.getAttribute('name') == 'options': + mmsdtng = True + self.mmsdtng = mmsdtng + return self.mmsdtng - def convert(self, path, dbus_path=None): + def send_mms(self, recipients, attachments, subject=None, smil=None): """ - Convert a provided mms file to a mail stored in a mbox. + Ask mmsd to send a MMS. - :param path: the mms filesystem path - :type path: str + :param recipients: The mms recipients phone numbers + :type recipients: Array(str) - :param dbus_path: the mms dbus path - :type dbus_path: 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 """ - # Check if the provided file present - if not self.check_mms(path): - print("MMS file not convertible.", file=sys.stderr) - return - # Generate its dbus path, for future operation (mark as read, delete) - if not dbus_path: - dbus_path = f"/org/ofono/mms/modemmanager/{path.split('/')[-1]}" + service = self.get_service() - mms = MMSMessage.from_file(path) - - self.mailer.start() - message = Message() - - # Generate Mail Headers - mms_from, mms_from_type = mms.headers.get('From', - 'unknown/undef').split('/') - message.author = f"{mms_from}@{self.domain}" - mms_from, mms_from_type = mms.headers.get('To', - 'unknown/undef').split('/') - message.to = f"{self.user}@{self.domain}" - - if 'Subject' in mms.headers and mms.headers['Subject']: - message.subject = mms.headers['Subject'] + mmsdtng = self.get_send_message_version() + if mmsdtng: + log.debug("Using mmsd-tng as backend") + option_list = {} + if subject: + log.debug(f"MMS Subject = {subject}") + option_list['Subject'] = GLib.Variant('s', subject) + if smil: + log.debug("Send MMS as Related") + option_list['smil'] = GLib.Variant('s', smil) + options = GLib.Variant('a{sv}', option_list) + path = service.SendMessage(recipients, options, + attachments) else: - message.subject = f"MMS from {mms_from}" + log.debug("Using mmsd as backend") + if smil: + log.debug("Send MMS as Related") + else: + log.debug("Send MMS as Mixed") + smil = "" + path = service.SendMessage(recipients, smil, + attachments) + log.debug(path) - if 'Date' in mms.headers and mms.headers['Date']: - message.date = mms.headers['Date'] - - # Recopy MMS HEADERS - for header in mms.headers: - message.headers.append((f"X-MMS-{header}", - f"{mms.headers[header]}")) - - message.plain = " " - data_id = 1 - for data_part in mms.data_parts: - datacontent = data_part.headers['Content-Type'] - if datacontent is not None: - if 'text/plain' in datacontent[0]: - encoding = datacontent[1].get('Charset', 'utf-8') - plain = data_part.data.decode(encoding) - message.plain += plain + '\n' - continue - extension = mimetypes.guess_extension(datacontent[0]) - filename = datacontent[1].get('Name', str(data_id)) - message.attach(filename + extension, data_part.data) - data_id = data_id + 1 - - # Add MMS binary file, for debugging purpose or reparsing in the future - if self.attach_mms: - message.attach(path, None, None, None, False, path.split('/')[-1]) - - # Creating an empty file stating the mms as been converted - self.mark_mms_read(dbus_path) - - if self.delete: - self.delete_mms(dbus_path) - - # Write the mail - self.mailer.send(message) - self.mailer.stop() - - -class FSWatcher: - """ - Use OS filesystem notification to watch for new MMS (DEPRECATED). - - Events are send to the FSHandler class - """ - - # Path to modemmanager storage - mms_folder = f"{Path.home()}/.mms/modemmanager" - - def __init__(self): - """Construct an instance.""" - self.observer = Observer() - self.patternold = re.compile('[0-9A-F]{40}$') - self.pattern = re.compile('[0-9a-f]{36}$') - - def is_mmsd_mms_file(self, path): - """ - Test if the provided file seems to be a mms file created by mmsd. - - :param path: the mms filesystem path - :type path: str - - :rtype boolean - :return: the test result - """ - if self.pattern.search(path) or self.patternold.search(path): + def add_signal_receiver(self): + """Add a signal receiver to the current bus.""" + if self.mms2mail: + service = self.get_service() + service.onMessageAdded = self.mms2mail.message_added return True else: return False def run(self): - """Run the watcher mainloop.""" - event_handler = FSHandler() - self.observer.schedule(event_handler, self.mms_folder, recursive=False) - self.observer.start() - try: - while True: - time.sleep(5) - finally: - self.observer.stop() - self.observer.join() - - -class FSHandler(FileSystemEventHandler): - """Handle the FSWatcher event.""" - - @staticmethod - def on_any_event(event): - """Trigger conversion on event by the FSWatcher.""" - if event.is_directory: - return None - elif event.event_type == 'created' or event.event_type == 'modified': - if w.is_mmsd_mms_file(event.src_path): - print(f"New MMS found : {event.src_path}.", file=sys.stderr) - m.convert(event.src_path) - elif event.event_type == 'moved': - if w.is_mmsd_mms_file(event.dest_path): - print(f"New MMS found : {event.dest_path}.", file=sys.stderr) - m.convert(event.dest_path) - - -class DbusWatcher(): - """Use DBus Signal notification to watch for new MMS.""" - - def run(self): - """Run the watcher mainloop.""" - bus = m.get_bus() - bus.add_signal_receiver(self.message_added, - bus_name="org.ofono.mms", - signal_name="MessageAdded", - member_keyword="member", - path_keyword="path", - interface_keyword="interface") + """Run the dbus mainloop.""" mainloop = GLib.MainLoop() - mainloop.run() - - def message_added(self, name, value, member, path, interface): - """Trigger conversion on MessageAdded signal.""" - if value['Status'] == 'downloaded' or value['Status'] == 'received': - print(f"New incoming MMS found ({name.split('/')[-1]})") - m.convert(value['Attachments'][0][2], name) - else: - print(f"New outgoing MMS found ({name.split('/')[-1]})") + log.info("Starting DBus watcher mainloop") + try: + mainloop.run() + except KeyboardInterrupt: + log.info("Stopping DBus watcher mainloop") + mainloop.quit() -if __name__ == '__main__': +def main(): + """Run the different functions handling mms and mail.""" parser = argparse.ArgumentParser() - mode = parser.add_mutually_exclusive_group() - mode.add_argument("-d", "--daemon", - help="Use dbus signal from mmsd by default but can also \ - watch mmsd storage folder (useful for mmsd < 1.0)", - nargs="?", default="dbus", - choices=['dbus', 'filesystem'], dest='watcher') - mode.add_argument("-f", "--file", nargs='+', - help="Parse specified mms files and quit", dest='files') - parser.add_argument('--delete', action='store_true', dest='delete', - help="Ask mmsd to delete the converted MMS") + 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('--force-read', action='store_true', dest='force_read', help="Force conversion even if MMS \ is marked as read") + parser.add_argument('--force-unlock', action='store_true', + dest='force_unlock', help="BEWARE COULD LEAD TO \ + WHOLE MBOX CORRUPTION \ + Force unlocking the mbox \ + after a few minutes /!\\") + parser.add_argument('-l', '--logging', dest='log_level', default='warning', + choices=['critical', 'error', 'warning', + 'info', 'debug'], + help='Define the logger output level' + ) + args = parser.parse_args() - m = MMS2Mail(args.delete, args.force_read) + log.setLevel(args.log_level.upper()) + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG) + log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + formatter = logging.Formatter(log_format) + ch.setFormatter(formatter) + log.addHandler(ch) - if args.files: - for mms_file in args.files: - m.convert(mms_file) - elif args.watcher == 'dbus': - print("Starting mms2mail in daemon mode with dbus watcher") - w = DbusWatcher() - w.run() - elif args.watcher == 'filesystem': - print("Starting mms2mail in daemon mode with filesystem watcher") - w = FSWatcher() - w.run() - else: - parser.print_help() + c = Config() + d = DbusMMSd() + + 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, + force_read=args.force_read, + force_unlock=args.force_unlock) + m.set_dbus(d) + + log.info("Starting mms2mail") + 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() + m.convert_stored_mms() + + d.run() + controller.stop() + + +if __name__ == '__main__': + main() diff --git a/mms2mail.ini b/mms2mail.ini index 4ec222e..fa4f363 100644 --- a/mms2mail.ini +++ b/mms2mail.ini @@ -3,3 +3,8 @@ mailbox = /var/mail/mobian account = mobian domain = mobian.lan attach_mms = false +delete_from_mmsd = false + +[smtp] +hostname = localhost +port = 2525 diff --git a/mms2mail.service b/mms2mail.service index 82703d8..5982731 100644 --- a/mms2mail.service +++ b/mms2mail.service @@ -3,7 +3,7 @@ Description=Multimedia Messaging Service to Mail converter Daemon After=mmsd.service [Service] -ExecStart=python3 %h/.local/bin/mms2mail -d dbus +ExecStart=python3 %h/.local/bin/mms2mail Restart=on-failure RestartSec=10s diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..30c9e7a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pydbus +aiosmtpd