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