[fix] Better handling mbox lock

- use only marrow.mailer Message class
 - use python logging
This commit is contained in:
Alex 2021-05-08 10:29:07 +02:00
parent fb8c22a535
commit 815b27ae23
2 changed files with 98 additions and 33 deletions

View File

@ -1,20 +1,24 @@
# mms2mail # 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 ## installation
### dependency ### dependency
- python3 - python3
- python3-watchdog (pip install watchdog)
- python-messaging (pip install python-messaging) - python-messaging (pip install python-messaging)
- marrow.mailer (pip install marrow.mailer) - marrow.mailer (pip install marrow.mailer)
### setup ### setup
Install the dependency and mms2mail: Install the dependency and mms2mail:
``` ```
pip install --user watchdog # or sudo apt install python3-watchdog
pip install --user marrow-mailer pip install --user marrow-mailer
pip install --user python-messaging pip install --user python-messaging
@ -42,16 +46,16 @@ attach_mms = false ; whether to attach the full mms binary file
``` ```
## usage ## 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: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-d [{dbus,filesystem}], --daemon [{dbus,filesystem}] -d, --daemon Use dbus signal from mmsd to trigger conversion
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 ...] -f FILES [FILES ...], --file FILES [FILES ...]
Parse specified mms files and quit Parse specified mms files and quit
--delete Ask mmsd to delete the converted MMS --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-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 /!\
``` ```

113
mms2mail
View File

@ -28,15 +28,19 @@ import configparser
import getpass import getpass
import socket import socket
import mimetypes import mimetypes
import time
from pathlib import Path from pathlib import Path
from messaging.mms.message import MMSMessage from messaging.mms.message import MMSMessage
from marrow.mailer import Mailer, Message import mailbox
from marrow.mailer import Message
from gi.repository import GLib from gi.repository import GLib
import dbus import dbus
import dbus.mainloop.glib import dbus.mainloop.glib
log = __import__('logging').getLogger(__name__)
class MMS2Mail: class MMS2Mail:
""" """
@ -46,7 +50,8 @@ class MMS2Mail:
Mail support is provided by marrow.mailer 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. Return class instance.
@ -55,9 +60,17 @@ class MMS2Mail:
:param force_read: force converting an already read MMS (batch mode) :param force_read: force converting an already read MMS (batch mode)
:type force_read: bool :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.delete = delete
self.force_read = force_read self.force_read = force_read
self.disable_dbus = disable_dbus
self.force_unlock = force_unlock
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini") self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini")
self.attach_mms = self.config.getboolean('mail', 'attach_mms', 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()) self.user = self.config.get('mail', 'user', fallback=getpass.getuser())
mbox_file = self.config.get('mail', 'mailbox', mbox_file = self.config.get('mail', 'mailbox',
fallback=f"/var/mail/{self.user}") fallback=f"/var/mail/{self.user}")
self.mailer = Mailer({'manager.use': 'immediate', self.mailbox = mailbox.mbox(mbox_file)
'transport.use': 'mbox', if self.disable_dbus:
'transport.file': mbox_file}) return
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
self.bus = dbus.SessionBus() self.bus = dbus.SessionBus()
@ -80,6 +93,8 @@ class MMS2Mail:
:rtype dbus.SessionBus() :rtype dbus.SessionBus()
:return: an active SessionBus :return: an active SessionBus
""" """
if self.disable_dbus:
return None
return self.bus return self.bus
def mark_mms_read(self, dbus_path): def mark_mms_read(self, dbus_path):
@ -89,10 +104,12 @@ class MMS2Mail:
:param dbus_path: the mms dbus path :param dbus_path: the mms dbus path
:type dbus_path: str :type dbus_path: str
""" """
if self.disable_dbus:
return None
message = dbus.Interface(self.bus.get_object('org.ofono.mms', message = dbus.Interface(self.bus.get_object('org.ofono.mms',
dbus_path), dbus_path),
'org.ofono.mms.Message') '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() message.MarkRead()
def delete_mms(self, dbus_path): def delete_mms(self, dbus_path):
@ -102,10 +119,12 @@ class MMS2Mail:
:param dbus_path: the mms dbus path :param dbus_path: the mms dbus path
:type dbus_path: str :type dbus_path: str
""" """
if self.disable_dbus:
return None
message = dbus.Interface(self.bus.get_object('org.ofono.mms', message = dbus.Interface(self.bus.get_object('org.ofono.mms',
dbus_path), dbus_path),
'org.ofono.mms.Message') 'org.ofono.mms.Message')
print(f"Deleting MMS {dbus_path}", file=sys.stderr) log.debug(f"Deleting MMS {dbus_path}")
message.Delete() message.Delete()
def check_mms(self, path): def check_mms(self, path):
@ -118,21 +137,21 @@ class MMS2Mail:
""" """
# Check for mmsd data file # Check for mmsd data file
if not Path(f"{path}").is_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 return False
# Check for mmsd status file # Check for mmsd status file
status = configparser.ConfigParser() status = configparser.ConfigParser()
if not Path(f"{path}.status").is_file(): 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 return False
status.read_file(open(f"{path}.status")) status.read_file(open(f"{path}.status"))
# Allow only incoming MMS for the time beeing # Allow only incoming MMS for the time beeing
if not (status['info']['state'] == 'downloaded' or if not (status['info']['state'] == 'downloaded' or
status['info']['state'] == 'received'): status['info']['state'] == 'received'):
print("Outgoing MMS : aborting", file=sys.stderr) log.error("Outgoing MMS : aborting")
return False return False
if not (self.force_read or not status.getboolean('info', 'read')): 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 False
return True return True
@ -148,7 +167,7 @@ class MMS2Mail:
""" """
# Check if the provided file present # Check if the provided file present
if not self.check_mms(path): if not self.check_mms(path):
print("MMS file not convertible.", file=sys.stderr) log.error("MMS file not convertible.")
return return
# Generate its dbus path, for future operation (mark as read, delete) # Generate its dbus path, for future operation (mark as read, delete)
if not dbus_path: if not dbus_path:
@ -156,7 +175,6 @@ class MMS2Mail:
mms = MMSMessage.from_file(path) mms = MMSMessage.from_file(path)
self.mailer.start()
message = Message() message = Message()
# Generate Mail Headers # Generate Mail Headers
@ -190,7 +208,7 @@ class MMS2Mail:
plain = data_part.data.decode(encoding) plain = data_part.data.decode(encoding)
message.plain += plain + '\n' message.plain += plain + '\n'
continue continue
extension = mimetypes.guess_extension(datacontent[0]) extension = str(mimetypes.guess_extension(datacontent[0]))
filename = datacontent[1].get('Name', str(data_id)) filename = datacontent[1].get('Name', str(data_id))
message.attach(filename + extension, data_part.data) message.attach(filename + extension, data_part.data)
data_id = data_id + 1 data_id = data_id + 1
@ -199,16 +217,50 @@ class MMS2Mail:
if self.attach_mms: if self.attach_mms:
message.attach(path, None, None, None, False, path.split('/')[-1]) message.attach(path, None, None, None, False, path.split('/')[-1])
# Creating an empty file stating the mms as been converted # Write the mail in case of mbox lock retry for 5 minutes
self.mark_mms_read(dbus_path) # 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: if self.delete:
self.delete_mms(dbus_path) self.delete_mms(dbus_path)
# Write the mail
self.mailer.send(message)
self.mailer.stop()
class DbusWatcher(): class DbusWatcher():
"""Use DBus Signal notification to watch for new MMS.""" """Use DBus Signal notification to watch for new MMS."""
@ -232,20 +284,20 @@ class DbusWatcher():
path_keyword="path", path_keyword="path",
interface_keyword="interface") interface_keyword="interface")
mainloop = GLib.MainLoop() mainloop = GLib.MainLoop()
print("Starting DBus watcher mainloop") log.info("Starting DBus watcher mainloop")
try: try:
mainloop.run() mainloop.run()
except KeyboardInterrupt: except KeyboardInterrupt:
print("Stopping DBus watcher mainloop") log.info("Stopping DBus watcher mainloop")
mainloop.quit() mainloop.quit()
def message_added(self, name, value, member, path, interface): def message_added(self, name, value, member, path, interface):
"""Trigger conversion on MessageAdded signal.""" """Trigger conversion on MessageAdded signal."""
if value['Status'] == 'downloaded' or value['Status'] == 'received': 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) self.mms2mail.convert(value['Attachments'][0][2], name)
else: else:
print(f"New outgoing MMS found ({name.split('/')[-1]})") log.debug(f"New outgoing MMS found ({name.split('/')[-1]})")
def main(): def main():
@ -259,18 +311,27 @@ def main():
help="Parse specified mms files and quit", dest='files') help="Parse specified mms files and quit", dest='files')
parser.add_argument('--delete', action='store_true', dest='delete', parser.add_argument('--delete', action='store_true', dest='delete',
help="Ask mmsd to delete the converted MMS") 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', parser.add_argument('--force-read', action='store_true',
dest='force_read', help="Force conversion even if MMS \ dest='force_read', help="Force conversion even if MMS \
is marked as read") 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() 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: if args.files:
for mms_file in args.files: for mms_file in args.files:
m.convert(mms_file) m.convert(mms_file)
elif args.watcher: elif args.watcher:
print("Starting mms2mail in daemon mode") log.info("Starting mms2mail in daemon mode")
w = DbusWatcher(m) w = DbusWatcher(m)
w.run() w.run()
else: else: