diff --git a/README.rst b/README.rst index 52a0e56..03d9477 100644 --- a/README.rst +++ b/README.rst @@ -274,8 +274,6 @@ The full set of configuration options are: - ``gmail_api`` - ``gmail_api_credentials_file`` - str: Path to file containing the credentials, None to disable (Default: None) - ``gmail_api_token_file`` - str: Path to save the token file (Default: .token) - - ``gmail_api_reports_label`` - str: Label to use when searching for reports to parse (Default: INBOX) - - ``gmail_api_archive_file`` - str: Label to apply to processed reports (Default: DMARC Archive) - ``gmail_api_include_spam_trash`` - bool: Include messages in Spam and Trash when searching reports (Default: False) - ``gmail_api_scopes`` - str: Comma separated list of scopes to use when acquiring credentials (Default: https://www.googleapis.com/auth/gmail.modify) diff --git a/parsedmarc/cli.py b/parsedmarc/cli.py index 99391ae..e025f5f 100644 --- a/parsedmarc/cli.py +++ b/parsedmarc/cli.py @@ -810,21 +810,21 @@ def _main(): exit(1) if opts.gmail_api_credentials_file: - if opts.gmail_api_delete: + if opts.mailbox_delete: if 'https://mail.google.com/' not in opts.gmail_api_scopes: logger.error("Message deletion requires scope" " 'https://mail.google.com/'. " "Add the scope and remove token file " "to acquire proper access.") - opts.gmail_api_delete = False + opts.mailbox_delete = False try: - token_file = opts.gmail_api_token_file or "token.json" mailbox_connection = GmailConnection( credentials_file=opts.gmail_api_credentials_file, - token_file=token_file, + token_file=opts.gmail_api_token_file, scopes=opts.gmail_api_scopes, include_spam_trash=opts.gmail_api_include_spam_trash, + reports_folder=opts.mailbox_reports_folder ) except Exception as error: diff --git a/parsedmarc/mail/gmail.py b/parsedmarc/mail/gmail.py index eaa40ef..9099353 100644 --- a/parsedmarc/mail/gmail.py +++ b/parsedmarc/mail/gmail.py @@ -1,5 +1,6 @@ import logging from base64 import urlsafe_b64decode +from functools import lru_cache from pathlib import Path from time import sleep from typing import List @@ -8,6 +9,7 @@ from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build +from googleapiclient.errors import HttpError from parsedmarc.mail.mailbox_connection import MailboxConnection @@ -19,6 +21,7 @@ def _get_creds(token_file, credentials_file, scopes): if Path(token_file).exists(): creds = Credentials.from_authorized_user_file(token_file, scopes) + # If there are no (valid) credentials available, let the user log in. if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: @@ -26,9 +29,9 @@ def _get_creds(token_file, credentials_file, scopes): else: flow = InstalledAppFlow.from_client_secrets_file( credentials_file, scopes) - creds = flow.run_console() + creds = flow.run_local_server(open_browser=False) # Save the credentials for the next run - with open(token_file, 'w') as token: + with Path(token_file).open('w') as token: token.write(creds.to_json()) return creds @@ -38,16 +41,29 @@ class GmailConnection(MailboxConnection): token_file: str, credentials_file: str, scopes: List[str], - include_spam_trash: bool): + include_spam_trash: bool, + reports_folder: str): creds = _get_creds(token_file, credentials_file, scopes) self.service = build('gmail', 'v1', credentials=creds) self.include_spam_trash = include_spam_trash + self.reports_label_id = self._find_label_id_for_label(reports_folder) def create_folder(self, folder_name: str): - logger.debug("Creating label {0}".format(folder_name)) + # Gmail doesn't support the name Archive + if folder_name == 'Archive': + return + + logger.debug(f"Creating label {folder_name}") request_body = {'name': folder_name, 'messageListVisibility': 'show'} - self.service.users().labels()\ - .create(userId='me', body=request_body).execute() + try: + self.service.users().labels()\ + .create(userId='me', body=request_body).execute() + except HttpError as e: + if e.status_code == 409: + logger.debug(f'Folder {folder_name} already exists, ' + f'skipping creation') + else: + raise e def fetch_messages(self, reports_folder: str) -> List[str]: reports_label_id = self._find_label_id_for_label(reports_folder) @@ -75,7 +91,10 @@ class GmailConnection(MailboxConnection): def move_message(self, message_id: str, folder_name: str): label_id = self._find_label_id_for_label(folder_name) logger.debug(f"Moving message UID {message_id} to {folder_name}") - request_body = {'addLabelIds': [label_id]} + request_body = { + 'addLabelIds': [label_id], + 'removeLabelIds': [self.reports_label_id] + } self.service.users().messages()\ .modify(userId='me', id=message_id, @@ -92,6 +111,7 @@ class GmailConnection(MailboxConnection): sleep(check_timeout) check_callback(self) + @lru_cache(maxsize=10) def _find_label_id_for_label(self, label_name: str) -> str: results = self.service.users().labels().list(userId='me').execute() labels = results.get('labels', []) diff --git a/parsedmarc/mail/graph.py b/parsedmarc/mail/graph.py index a26a9a3..2237b04 100644 --- a/parsedmarc/mail/graph.py +++ b/parsedmarc/mail/graph.py @@ -96,7 +96,7 @@ class MSGraphConnection(MailboxConnection): sleep(check_timeout) check_callback(self) - @lru_cache(maxsize=100) + @lru_cache(maxsize=10) def _find_folder_id_from_folder_path(self, folder_name: str) -> str: path_parts = folder_name.split('/') parent_folder_id = None