diff --git a/README.md b/README.md index a6609a1..b0eec5a 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,11 @@ in case the mbox is locked by another process the output could be found in : - python3 - python-messaging (pip install python-messaging) - - marrow.mailer (pip install marrow.mailer) ### setup Install the dependency and mms2mail: ``` -pip install --user marrow-mailer +sudo apt-get install python3 pip install --user python-messaging mkdir -p ~/.local/bin @@ -47,7 +46,8 @@ attach_mms = false ; whether to attach the full mms binary file ## usage ``` -mms2mail [-h] [-d | -f FILES [FILES ...]] [--delete] [--disable-dbus] [--force-read] [--force-unlock] +mms2mail [-h] [-d | -f FILES [FILES ...]] [--delete] [--force-read] + [--force-unlock] [-l {critical,error,warning,info,debug}] optional arguments: -h, --help show this help message and exit @@ -55,7 +55,9 @@ optional arguments: -f FILES [FILES ...], --file FILES [FILES ...] Parse specified mms files and quit --delete Ask mmsd to delete the converted MMS - --disable-dbus disable dbus request to mmsd --force-read Force conversion even if MMS is marked as read - --force-unlock BEWARE COULD LEAD TO WHOLE MBOX CORRUPTION Force unlocking the mbox after a few minutes /!\ + --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 ``` \ No newline at end of file diff --git a/mms2mail b/mms2mail index 75fa5c0..24ec2e9 100755 --- a/mms2mail +++ b/mms2mail @@ -29,17 +29,18 @@ import getpass import socket import mimetypes import time +import logging from pathlib import Path from messaging.mms.message import MMSMessage import mailbox -from marrow.mailer import Message +import email from gi.repository import GLib import dbus import dbus.mainloop.glib -log = __import__('logging').getLogger(__name__) +log = logging.getLogger(__name__) class MMS2Mail: @@ -51,7 +52,7 @@ class MMS2Mail: """ def __init__(self, delete=False, force_read=False, - disable_dbus=False, force_unlock=False): + force_unlock=False): """ Return class instance. @@ -69,7 +70,6 @@ class MMS2Mail: """ self.delete = delete self.force_read = force_read - self.disable_dbus = disable_dbus self.force_unlock = force_unlock self.config = configparser.ConfigParser() self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini") @@ -98,37 +98,35 @@ class MMS2Mail: :param path: the mms filesystem path :type path: str - :rtype bool + + :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 False + 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 False + return None 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'): - log.error("Outgoing MMS : aborting") - return False if not (self.force_read or not status.getboolean('info', 'read')): log.error("Already converted MMS : aborting") - return False - return True + return None + return status['info']['state'] def message_added(self, name, value, member, path, interface): """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(value['Attachments'][0][2], name) + 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=None): + def convert(self, path, dbus_path=None, properties=None): """ Convert a provided mms file to a mail stored in a mbox. @@ -139,7 +137,8 @@ class MMS2Mail: :type dbus_path: str """ # Check if the provided file present - if not self.check_mms(path): + status = self.check_mms(path) + if not status: log.error("MMS file not convertible.") return # Generate its dbus path, for future operation (mark as read, delete) @@ -147,48 +146,83 @@ class MMS2Mail: dbus_path = f"/org/ofono/mms/modemmanager/{path.split('/')[-1]}" mms = MMSMessage.from_file(path) - - message = Message() + message = email.message.EmailMessage() # 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}" + mms_h_from = mms.headers.get('From', 'unknown/undef') + log.debug(f"MMS[From]: {mms_h_from}") + if 'not inserted' in mms_h_from: + mms_h_from = 'unknown/undef' + mms_from, mms_from_type = mms_h_from.split('/') + message['From'] = f"{mms_from}@{self.domain}" + + mms_h_to = mms.headers.get('To', 'unknown/undef') + log.debug(f"MMS[To]: {mms_h_to}") + if 'not inserted' in mms_h_to: + mms_h_to = 'unknown/undef' + mms_to, mms_to_type = mms_h_to.split('/') + message['To'] = f"{mms_to}@{self.domain}" + + # Get other recipients from dbus signal + # https://github.com/pmarti/python-messaging/issues/49 + if properties: + cc = "" + for r in properties['Recipients']: + if mms_to in r: + continue + log.debug(f'MMS/MAIL CC : {r}') + cc += f"{r}@{self.domain}," + if cc: + cc = cc[:-1] + message['CC'] = cc if 'Subject' in mms.headers and mms.headers['Subject']: - message.subject = mms.headers['Subject'] + message['Subject'] = mms.headers['Subject'] else: - message.subject = f"MMS from {mms_from}" + if status == 'sent' or status == 'draft': + message['Subject'] = f"MMS to {mms_to}" + else: + message['Subject'] = f"MMS from {mms_from}" if 'Date' in mms.headers and mms.headers['Date']: - message.date = 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.add_header(f"X-MMS-{header}", f"{mms.headers[header]}") - message.plain = " " + message.preamble = "This mail is converted from a MMS." + body = "" data_id = 1 + attachments = [] for data_part in mms.data_parts: datacontent = data_part.headers['Content-Type'] if datacontent is not None: + maintype, subtype = datacontent[0].split('/', 1) if 'text/plain' in datacontent[0]: encoding = datacontent[1].get('Charset', 'utf-8') - plain = data_part.data.decode(encoding) - message.plain += plain + '\n' + body += data_part.data.decode(encoding) + '\n' continue extension = str(mimetypes.guess_extension(datacontent[0])) filename = datacontent[1].get('Name', str(data_id)) - message.attach(filename + extension, data_part.data) + attachments.append([data_part.data, maintype, + subtype, filename + extension]) data_id = data_id + 1 + 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: - message.attach(path, None, None, None, False, path.split('/')[-1]) + 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 @@ -197,7 +231,7 @@ class MMS2Mail: try: # self.mailer.send(message) self.mailbox.lock() - self.mailbox.add(mailbox.mboxMessage(str(message))) + self.mailbox.add(mailbox.mboxMessage(message)) self.mailbox.flush() self.mailbox.unlock() break @@ -228,13 +262,11 @@ class MMS2Mail: else: time.sleep(5) - # Ask mmsd to mark message as read and delete it - if self.disable_dbus: - return - self.dbus.mark_mms_read(dbus_path) - if self.delete: - self.dbus.delete_mms(dbus_path) + if properties: + self.dbus.mark_mms_read(dbus_path) + if self.delete: + self.dbus.delete_mms(dbus_path) class DbusMMSd(): @@ -267,9 +299,9 @@ class DbusMMSd(): :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') + message = dbus.proxies.Interface(self.bus.get_object('org.ofono.mms', + dbus_path), + 'org.ofono.mms.Message') log.debug(f"Marking MMS as read {dbus_path}") message.MarkRead() @@ -282,9 +314,9 @@ class DbusMMSd(): """ if self.disable_dbus: return None - message = dbus.Interface(self.bus.get_object('org.ofono.mms', - dbus_path), - 'org.ofono.mms.Message') + message = dbus.proxies.Interface(self.bus.get_object('org.ofono.mms', + dbus_path), + 'org.ofono.mms.Message') log.debug(f"Deleting MMS {dbus_path}") message.Delete() @@ -323,9 +355,6 @@ def main(): 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-dbus', action='store_true', - dest='disable_dbus', - help="disable dbus request to mmsd") parser.add_argument('--force-read', action='store_true', dest='force_read', help="Force conversion even if MMS \ is marked as read") @@ -334,16 +363,31 @@ def main(): 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() + 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) + d = DbusMMSd() - m = MMS2Mail(args.delete, args.force_read, - args.disable_dbus, args.force_unlock) + m = MMS2Mail(delete=args.delete, force_read=args.force_read, + force_unlock=args.force_unlock) m.set_dbus(d) if args.files: for mms_file in args.files: - m.convert(mms_file) + m.convert(path=mms_file) + return elif args.watcher: log.info("Starting mms2mail in daemon mode") d.set_mms2mail(m)