diff --git a/README.md b/README.md index 7e32662..a6609a1 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,24 @@ # mms2mail -Convert MMSd MMS file to mbox. +Convert mmsd MMS file to mbox. + +mms2mail can convert mms in batch mode, or wait for new mms via a dbus signal +sent by mmsd. +By default it store them 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``` ## installation ### dependency - python3 - - python3-watchdog (pip install watchdog) - python-messaging (pip install python-messaging) - marrow.mailer (pip install marrow.mailer) ### setup Install the dependency and mms2mail: ``` -pip install --user watchdog # or sudo apt install python3-watchdog pip install --user marrow-mailer pip install --user python-messaging @@ -42,16 +46,16 @@ attach_mms = false ; whether to attach the full mms binary file ``` ## usage - ``` -mms2mail [-h] [-d [{dbus,filesystem}] | -f FILES [FILES ...]] [--delete] [--force-read] +mms2mail [-h] [-d | -f FILES [FILES ...]] [--delete] [--disable-dbus] [--force-read] [--force-unlock] 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) + -d, --daemon Use dbus signal from mmsd to trigger conversion -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 /!\ ``` \ No newline at end of file diff --git a/mms2mail b/mms2mail index a8fa85c..3f4c2e8 100755 --- a/mms2mail +++ b/mms2mail @@ -28,15 +28,19 @@ import configparser import getpass import socket import mimetypes +import time from pathlib import Path from messaging.mms.message import MMSMessage -from marrow.mailer import Mailer, Message +import mailbox +from marrow.mailer import Message from gi.repository import GLib import dbus import dbus.mainloop.glib +log = __import__('logging').getLogger(__name__) + class MMS2Mail: """ @@ -46,7 +50,8 @@ class MMS2Mail: Mail support is provided by marrow.mailer """ - def __init__(self, delete=False, force_read=False): + def __init__(self, delete=False, force_read=False, + disable_dbus=False, force_unlock=False): """ Return class instance. @@ -55,9 +60,17 @@ class MMS2Mail: :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.disable_dbus = disable_dbus + 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', @@ -67,9 +80,9 @@ class MMS2Mail: 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}) + self.mailbox = mailbox.mbox(mbox_file) + if self.disable_dbus: + return dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) self.bus = dbus.SessionBus() @@ -80,6 +93,8 @@ class MMS2Mail: :rtype dbus.SessionBus() :return: an active SessionBus """ + if self.disable_dbus: + return None return self.bus def mark_mms_read(self, dbus_path): @@ -89,10 +104,12 @@ class MMS2Mail: :param dbus_path: the mms dbus path :type dbus_path: str """ + if self.disable_dbus: + return None 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) + log.debug(f"Marking MMS as read {dbus_path}") message.MarkRead() def delete_mms(self, dbus_path): @@ -102,10 +119,12 @@ class MMS2Mail: :param dbus_path: the mms dbus path :type dbus_path: str """ + if self.disable_dbus: + return None 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) + log.debug(f"Deleting MMS {dbus_path}") message.Delete() def check_mms(self, path): @@ -118,21 +137,21 @@ class MMS2Mail: """ # Check for mmsd data file if not Path(f"{path}").is_file(): - print("MMS file not found : aborting", file=sys.stderr) + log.error("MMS file not found : aborting") 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) + log.error("MMS status file not found : aborting") 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) + log.error("Outgoing MMS : aborting") return False if not (self.force_read or not status.getboolean('info', 'read')): - print("Already converted MMS : aborting", file=sys.stderr) + log.error("Already converted MMS : aborting") return False return True @@ -148,7 +167,7 @@ class MMS2Mail: """ # Check if the provided file present if not self.check_mms(path): - print("MMS file not convertible.", file=sys.stderr) + log.error("MMS file not convertible.") return # Generate its dbus path, for future operation (mark as read, delete) if not dbus_path: @@ -156,7 +175,6 @@ class MMS2Mail: mms = MMSMessage.from_file(path) - self.mailer.start() message = Message() # Generate Mail Headers @@ -190,7 +208,7 @@ class MMS2Mail: plain = data_part.data.decode(encoding) message.plain += plain + '\n' continue - extension = mimetypes.guess_extension(datacontent[0]) + extension = str(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 @@ -199,16 +217,50 @@ class MMS2Mail: 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) + # 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(str(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 + 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 DbusWatcher(): """Use DBus Signal notification to watch for new MMS.""" @@ -232,20 +284,20 @@ class DbusWatcher(): path_keyword="path", interface_keyword="interface") mainloop = GLib.MainLoop() - print("Starting DBus watcher mainloop") + log.info("Starting DBus watcher mainloop") try: mainloop.run() except KeyboardInterrupt: - print("Stopping DBus watcher mainloop") + log.info("Stopping DBus watcher mainloop") mainloop.quit() 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]})") + log.debug(f"New incoming MMS found ({name.split('/')[-1]})") self.mms2mail.convert(value['Attachments'][0][2], name) else: - print(f"New outgoing MMS found ({name.split('/')[-1]})") + log.debug(f"New outgoing MMS found ({name.split('/')[-1]})") def main(): @@ -259,18 +311,27 @@ 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") + 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 /!\\") args = parser.parse_args() - m = MMS2Mail(args.delete, args.force_read) + m = MMS2Mail(args.delete, args.force_read, + args.disable_dbus, args.force_unlock) if args.files: for mms_file in args.files: m.convert(mms_file) elif args.watcher: - print("Starting mms2mail in daemon mode") + log.info("Starting mms2mail in daemon mode") w = DbusWatcher(m) w.run() else: