[feat] DBus

* Use DBus Signal by default in daemon mode
* Allow multiple files in standalone mode
* Better handle mms headers and content
This commit is contained in:
Alex 2021-05-04 12:58:23 +02:00 committed by Alex
parent f032511cac
commit df9d5acf76
3 changed files with 222 additions and 74 deletions

View file

@ -7,19 +7,16 @@ Convert MMSd MMS file to mbox.
### dependancy ### dependancy
- python3 - python3
- python3-watchdog (apt install python3-watchdog) - python3-watchdog (pip install watchdog)
- python-messaging (https://www.github.com/davegermiquet/python-messaging.git) - python-messaging (pip install python-messaging)
- marrow.mailer (pip install marrow.mailer) - marrow.mailer (pip install marrow.mailer)
### setup ### 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 pip install --user marrow-mailer
pip install --user python-messaging
git clone https://www.github.com/davegermiquet/python-messaging.git
cd python-messaging
python3 setup.py install --user
mkdir -p ~/.local/bin mkdir -p ~/.local/bin
cp mms2mail ~/.local/bin cp mms2mail ~/.local/bin
@ -46,10 +43,11 @@ attach_mms = false ; whether to attach the full mms binary file
## usage ## usage
mms2mail [-h] [-d | -f FILE] mms2mail [-h] [-d [{dbus,filesystem}] | -f FILES [FILES ...]]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-d, --daemon watch for new mms in the mmsd storage folder -d [{dbus,filesystem}], --daemon [{dbus,filesystem}]
-f FILE, --file FILE parse a single mms file 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

272
mms2mail
View file

@ -1,4 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
"""
An mms to mail converter for mmsd
"""
#upstream bug dirty fix https://github.com/marrow/mailer/issues/87#issuecomment-689586587 #upstream bug dirty fix https://github.com/marrow/mailer/issues/87#issuecomment-689586587
import sys import sys
if sys.version_info[0] == 3 and sys.version_info[1] > 7: if sys.version_info[0] == 3 and sys.version_info[1] > 7:
@ -18,19 +21,175 @@ import time
import getpass import getpass
import socket import socket
from pathlib import Path from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from messaging.mms.message import MMSMessage from messaging.mms.message import MMSMessage
from marrow.mailer import Mailer, Message 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" mms_folder = f"{Path.home()}/.mms/modemmanager"
def __init__(self): def __init__(self):
self.observer = Observer() 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): 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.schedule(event_handler, self.mms_folder, recursive=False)
self.observer.start() self.observer.start()
try: try:
@ -43,84 +202,75 @@ class Watcher:
self.observer.join() self.observer.join()
class Handler(FileSystemEventHandler): class FSHandler(FileSystemEventHandler):
"""
Handle the FSWatcher event
"""
@staticmethod @staticmethod
def on_any_event(event): def on_any_event(event):
"""
Function triggered on event by the FSWatcher
"""
if event.is_directory: if event.is_directory:
return None return None
elif event.event_type == 'created' or event.event_type == 'modified': 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) print(f"New MMS found : {event.src_path}.", file=sys.stderr)
m.convert(event.src_path) m.convert(event.src_path)
elif event.event_type == 'moved': 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) print(f"New MMS found : {event.dest_path}.", file=sys.stderr)
m.convert(event.dest_path) 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): def message_added(self,name, value, member, path, interface):
self.config = configparser.ConfigParser() """
self.config.read(f"{Path.home()}/.mms/modemmanager/mms2mail.ini") Function triggered on MessageAdded signal
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}$') if value['Status'] == 'downloaded' or value['Status'] == 'received':
print(f"New incoming MMS found ({name.split('/')[-1]})")
def convert(self, path): m.convert(value['Attachments'][0][2],name)
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)
else: else:
print(f"New outgoing MMS : doing nothing ({path})", file=sys.stderr) print(f"New outgoing MMS found ({name.split('/')[-1]})")
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)
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
mode = parser.add_mutually_exclusive_group() 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("-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=1, help="parse a single mms file") mode.add_argument("-f","--file", nargs='+', help="parse specified mms files", dest='files')
args = parser.parse_args() args = parser.parse_args()
m = MMS2Mail() 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() w.run()
elif args.file:
m.convert(args.file[0])
else: else:
parser.print_help() parser.print_help()
del m del m

View file

@ -3,7 +3,7 @@ Description=Multimedia Messaging Service to Mail converter Daemon
After=mmsd.service After=mmsd.service
[Service] [Service]
ExecStart=python3 %h/.local/bin/mms2mail -d ExecStart=python3 %h/.local/bin/mms2mail -d dbus
Restart=on-failure Restart=on-failure
RestartSec=10s RestartSec=10s