#!/usr/bin/python3 import logging import os import re import sys from copy import copy from optparse import OptionParser, Option import dateutil.parser as dateparser from imbox import Imbox from jinja2 import Template, UndefinedError from attachment_downloader.cli import valid_date, get_password from attachment_downloader.encoding import QuoPriEncoding from attachment_downloader.logging import Logger from attachment_downloader.version_info import Version if __name__ == '__main__': Logger.setup() logging.info(f'Attachment Downloader - Version: {Version.get()} {Version.get_env_info()}') if sys.version_info[0] < 3: logging.error("This application requires Python 3+, you are running version: %s", sys.version) exit(1) class AttachmentDownloaderOption(Option): TYPES = Option.TYPES + ('date',) TYPE_CHECKER = copy(Option.TYPE_CHECKER) TYPE_CHECKER['date'] = valid_date parser = OptionParser(option_class=AttachmentDownloaderOption) parser.add_option("--host", dest="host", help="IMAP Host") parser.add_option("--username", dest="username", help="IMAP Username") parser.add_option("--password", dest="password", help="IMAP Password") parser.add_option("--imap-folder", dest="imap_folder", help="IMAP Folder to extract attachments from") parser.add_option("--subject-regex", dest="subject_regex", help="Regex that the subject must match against") parser.add_option("--date-after", dest="date_after", type='date', help='Select messages after this date') parser.add_option("--date-before", dest="date_before", type='date', help='Select messages before this date') parser.add_option("--filename-template", dest="filename_template", help="Attachment filename (jinja2) template.", default="{{ attachment_name }}") parser.add_option("--output", dest="download_folder", help="Output directory for attachment download") parser.add_option("--delete", dest="delete", action="store_true", help="Delete downloaded emails from Mailbox") parser.add_option("--delete-copy-folder", dest="delete_copy_folder", help="IMAP folder to copy emails to before deleting them") parser.add_option("--port", dest="port", type=int, help="Specify imap server port (defaults to 993 for TLS and 143 otherwise") parser.add_option("--unsecure", dest="unsecure", action="store_true", help="disable encrypted connection (not recommended)") parser.add_option("--starttls", dest="starttls", action="store_true", help="enable STARTTLS (not recommended)") (options, args) = parser.parse_args() if not options.host: parser.error('--host parameter required') if not options.username: parser.error('--username parameter required') if not options.download_folder: parser.error('--output parameter required') if options.delete_copy_folder and not options.delete: parser.error('--delete parameter required when using --delete-copy-folder') if options.unsecure and options.starttls: parser.error('--unsecure and --starttls are exclusive') tls = False if options.unsecure else True if options.starttls: starttls = True tls = False else: starttls = False filename_template = Template(options.filename_template) password = options.password if options.password else get_password() logging.info("Logging in to: '%s' as '%s'", options.host, options.username) mail = Imbox(options.host, username=options.username, password=password, port=options.port, ssl=tls, starttls=starttls) folders = [] if options.imap_folder: folders = [options.imap_folder] else: status, folders = mail.folders() folders = [folder.decode().split(" \"/\" ")[1] for folder in folders] for folder in folders: filter_options = {'folder': folder} if options.date_after and options.date_before and options.date_after.date() == options.date_before.date(): filter_options['date__on'] = options.date_after.date() else: if options.date_after: filter_options['date__gt'] = options.date_after.date() if options.date_before: filter_options['date__lt'] = options.date_before.date() logging.info("Listing messages matching the following criteria: %s", ", ".join([k + "=" + str(v) for k, v in filter_options.items()])) messages = mail.messages(**filter_options) for (uid, message) in messages: subject = '' if hasattr(message, 'subject'): subject = QuoPriEncoding.decode(message.subject) try: parsed_message_date = dateparser.parse(message.date.split("(")[0]) except Exception as e: logging.exception(e) if hasattr(message, 'date'): logging.error("Skipping message '%s' subject '%s' because date can not be parsed '%s'", uid, subject, message.date.split("(")[0]) else: logging.error("Skipping message '%s' subject '%s' because message does not have a date attribute", uid, subject) continue if options.date_after and parsed_message_date < options.date_after: logging.warning("Skipping message '%s' subject '%s' because it is before %s", uid, subject, options.date_after) continue if options.date_before and parsed_message_date >= options.date_before: logging.warning("Skipping message '%s' subject '%s' because it is after %s", uid, subject, options.date_before) continue if options.subject_regex and not re.match(options.subject_regex, subject): logging.warning("Skipping message '%s' subject '%s' because it does not match %s", uid, subject, options.subject_regex) continue logging.info("Processing message '%s' subject '%s'", uid, subject) for idx, attachment in enumerate(message.attachments): try: attachment_filename = QuoPriEncoding.decode(attachment.get('filename')) download_filename = filename_template.render(attachment_name=attachment_filename, attachment_idx=idx, subject=subject, message_id=message.message_id, date=parsed_message_date, folder=folder) download_path = os.path.join(options.download_folder, download_filename) os.makedirs(os.path.dirname(os.path.abspath(download_path)), exist_ok=True) logging.info("Downloading attachment '%s' to path %s", attachment_filename, download_path) if os.path.isfile(download_path): logging.warning("Overwriting file: '%s'", download_path) with open(download_path, "wb") as fp: fp.write(attachment.get('content').read()) except UndefinedError as e: logging.error('Filename template contains an undefined variable: %s', str(e)) logging.info('Logging out of: %s', options.host) mail.logout() exit(-1) except Exception as e: logging.exception(e) logging.error('Error saving file. Continuing...') else: if options.delete: if options.delete_copy_folder: mail.copy(uid, '"' + options.delete_copy_folder + '"') mail.delete(uid) logging.info('Finished processing messages') logging.info('Logging out of: %s', options.host) mail.logout() logging.info("Done")