diff --git a/README.md b/README.md index af0b473..b58b873 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,16 @@ Convert MMSd MMS file to mbox. ### dependancy - python3 - - python3-watchdog (apt install python3-watchdog) - - python-messaging (https://www.github.com/davegermiquet/python-messaging.git) + - python3-watchdog (pip install watchdog) + - python-messaging (pip install python-messaging) - marrow.mailer (pip install marrow.mailer) ### setup -Install the dependancy and mms2mail: +Install the dependency and mms2mail: ``` -sudo apt install python3-watchdog # pip install --user watchdog +pip install --user watchdog # or sudo apt install python3-watchdog pip install --user marrow-mailer - -git clone https://www.github.com/davegermiquet/python-messaging.git -cd python-messaging -python3 setup.py install --user +pip install --user python-messaging mkdir -p ~/.local/bin cp mms2mail ~/.local/bin @@ -46,10 +43,11 @@ attach_mms = false ; whether to attach the full mms binary file ## usage -mms2mail [-h] [-d | -f FILE] +mms2mail [-h] [-d [{dbus,filesystem}] | -f FILES [FILES ...]] optional arguments: -h, --help show this help message and exit - -d, --daemon watch for new mms in the mmsd storage folder - -f FILE, --file FILE parse a single mms file - + -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 diff --git a/mms2mail b/mms2mail index 9cb3fb4..5a236d3 100755 --- a/mms2mail +++ b/mms2mail @@ -1,4 +1,7 @@ #!/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: @@ -18,19 +21,175 @@ import time import getpass import socket from pathlib import Path -from watchdog.observers import Observer -from watchdog.events import FileSystemEventHandler + from messaging.mms.message import MMSMessage from marrow.mailer import Mailer, Message -class Watcher: +from gi.repository import GObject, GLib +import dbus +import dbus.mainloop.glib +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +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): + """ + Constructor loading configuration + """ + self.config = configparser.ConfigParser() + self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini") + self.mailer = Mailer({'manager.use': 'immediate', 'transport.use': 'mbox', 'transport.file': self.config.get('mail','mailbox', fallback=f"/var/mail/{getpass.getuser()}")}) + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + self.bus = dbus.SessionBus() + + def get_bus(self): + """ + Return the DBus SessionBus + + :rtype dbus.SessionBus() + :return: an active SessionBus + """ + return self.bus + + def mark_mms_read(self,dbus_path): + """ + Ask MMSd to mark the mms as read + + :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.MarkRead() + + def convert(self, path, dbus_path = None): + """ + 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 + """ + + # Check if the provided file present + if not Path(f"{path}").is_file(): + print(f"MMS file not found : aborting", 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]}" + if Path(f"{path}.mail").is_file() and args.watcher: + print(f"Already converted MMS : doing nothing ({path})", file=sys.stderr) + return + # Check for mmsd status file + status = configparser.ConfigParser() + if not Path(f"{path}.status").is_file(): + print(f"MMS status file not found : aborting", file=sys.stderr) + return + status.read_file(open(f"{path}.status")) + + # Allow only incoming MMS for the time beeing + if status['info']['state'] == 'downloaded' or status['info']['state'] == 'received': + print(f"New incoming MMS : converting to Mail ({path})", file=sys.stderr) + else: + print(f"New outgoing MMS : doing nothing ({path})", file=sys.stderr) + return + + mms = MMSMessage.from_file(path) + + self.mailer.start() + message = Message() + + # Generate Mail Headers + if 'From' in mms.headers and mms.headers['From']: + mms_from, mms_from_type = mms.headers['From'].split('/') + else: + mms_from = "unknown" + mms_from_type = "undef" + message.author = f"{mms_from}@{self.config.get('mail','domain', fallback=socket.getfqdn())}" + if 'To' in mms.headers and mms.headers['To']: + mms_to, mms_to_type = mms.headers['To'].split('/') + else: + mms_to = "unknown" + mms_to_type = "undef" + message.to = f"{self.config.get('mail','user', fallback=getpass.getuser())}@{self.config.get('mail','domain', fallback=socket.getfqdn())}" + + if 'Subject' in mms.headers and mms.headers['Subject']: + message.subject = mms.headers['Subject'] + else: + message.subject = f"MMS from {mms_from}" + + 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]}")) + + for data_part in mms.data_parts: + datacontent=data_part.headers['Content-Type'] + if datacontent is not None: + if 'text/plain' in datacontent[0]: + message.plain = f"{data_part.data} \n" + if 'Name' in datacontent[1]: + filename = datacontent[1]['Name'] + message.attach(filename,data_part.data) + # Ensure a proper body content in the resulting mail + if not message.plain: + message.plain = " " + # Add MMS binary file, for debugging purpose or reparsing in the future + if self.config.getboolean('mail','attach_mms', fallback=False): + message.attach(path, None, None, None, False, "mms.bin") + + # Creating an empty file stating the mms as been converted + Path(f"{path}.mail").touch() + + # 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): 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): + return True + else: + return False def run(self): - event_handler = Handler() + """ + Run the watcher mainloop + """ + event_handler = FSHandler() self.observer.schedule(event_handler, self.mms_folder, recursive=False) self.observer.start() try: @@ -43,84 +202,75 @@ class Watcher: self.observer.join() -class Handler(FileSystemEventHandler): - +class FSHandler(FileSystemEventHandler): + """ + Handle the FSWatcher event + """ @staticmethod def on_any_event(event): + """ + Function triggered on event by the FSWatcher + """ if event.is_directory: return None elif event.event_type == 'created' or event.event_type == 'modified': - if m.is_mmsd_mms_file(event.src_path): + 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 m.is_mmsd_mms_file(event.dest_path): + 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 MMS2Mail: +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") + mainloop = GLib.MainLoop() + mainloop.run() - def __init__(self): - self.config = configparser.ConfigParser() - self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini") - self.mailer = Mailer({'manager.use': 'immediate', 'transport.use': 'mbox', 'transport.file': self.config.get('mail','mailbox', fallback=f"/var/mail/{getpass.getuser()}")}) - self.pattern = re.compile('[0-9A-F]{40}$') - - def convert(self, path): - print(path, file=sys.stderr) - self.mailer.start() - if Path(f"{path}.mail").is_file() and args.daemon: - print(f"Already converted MMS : doing nothing ({path})", file=sys.stderr) - return - status = configparser.ConfigParser() - status.read_file(open(f"{path}.status")) - if status['info']['state'] == 'downloaded' or status['info']['state'] == 'received': - print(f"New incomming MMS : converting to Mail ({path})", file=sys.stderr) + def message_added(self,name, value, member, path, interface): + """ + Function triggered 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 : doing nothing ({path})", file=sys.stderr) - return - - mms = MMSMessage.from_file(path) - mms_from, mms_from_type = mms.headers['From'].split('/') - mms_to, mms_to_type = mms.headers['To'].split('/') - - message = Message(author=f"{mms_from}@{self.config.get('mail','domain', fallback=socket.getfqdn())}", to=f"{self.config.get('mail','user', fallback=getpass.getuser())}@{self.config.get('mail','domain', fallback=socket.getfqdn())}") - message.subject = f"MMS from {mms_from}" - message.date = mms.headers['Date'] - message.headers = [('X-MMS-From', mms.headers['From']), ('X-MMS-To', mms.headers['To']), ('X-MMS-ID', mms.headers['Message-ID'])] - message.plain = f"MMS from {mms_from}" - - if self.config.getboolean('mail','attach_mms', fallback=False): - message.attach(path, None, None, None, False, "mms.bin") - - for data_part in mms.data_parts: - datacontent=data_part.headers['Content-Type'] - if datacontent is not None: - if 'text/plain' in datacontent[0]: - message.plain = data_part.data - if 'Name' in datacontent[1]: - filename = datacontent[1]['Name'] - message.attach(filename,data_part.data) - Path(f"{path}.mail").touch() - self.mailer.send(message) - self.mailer.stop() - - def is_mmsd_mms_file(self,path): - return self.pattern.search(path) + print(f"New outgoing MMS found ({name.split('/')[-1]})") if __name__ == '__main__': parser = argparse.ArgumentParser() mode = parser.add_mutually_exclusive_group() - mode.add_argument("-d","--daemon", help="watch for new mms in the mmsd storage folder", action="store_true") - mode.add_argument("-f","--file", nargs=1, help="parse a single mms file") + 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", dest='files') args = parser.parse_args() m = MMS2Mail() - if args.daemon: - w = Watcher() + + 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() - elif args.file: - m.convert(args.file[0]) else: parser.print_help() del m + diff --git a/mms2mail.service b/mms2mail.service index 4f24954..82703d8 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 +ExecStart=python3 %h/.local/bin/mms2mail -d dbus Restart=on-failure RestartSec=10s