[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
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 /!\
```

113
mms2mail
View File

@ -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: