""" AUTHOR: Khushal P Soonderji DATE: Thursday, 16th Jan., 2025. OBJECTIVE: To handle all mail-related behaviour for Gmail from one place. REFERENCES: N/A DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append(".") sys.path.append("..") # My async utils: from utils_v2.string import json from utils_v2.mail import mail_parser from utils_v2.date_time import date_time from utils_v2.database.async_mysql_v2 import AsyncMySQL from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache from utils_v2.logging.context import AsyncLoggerContext # Controllers: from controllers_v2.message.mail.base import MailController # Models: from models.core.user import CoreUserInfoModel from models.core.auth_token import CoreAuthTokenModel from models.core.message import CoreMessageModel from models.api.message.mail.oauth import ( OAuthMailAuthorizationRequestHeaders, OAuthMailAuthorizationRequestData ) from models.message.mail.oauth import OAuthMailGetAuthorizationURLResponse, OAuthMailHandleCallbackResponse from models.message.mail.sync import MailSyncOneResult, MailSyncManyResults from models.message.mail.send import MailSendOneResult # Mail Client(s): from utils_v2.goog.controllers.gmail.gmail_client import AsyncGmailClient, SCOPES_GMAIL_MAIL_MANAGEMENT from utils_v2.goog.controllers.gmail.gmail_message import GmailMessage from utils_v2.goog.models.auth_tokens import GoogleAuthTokens # Shared: from shared import constants # To work with datatypes: from typing import List, Any # To work with MongoDB: from bson import ObjectId from pymongo import InsertOne, UpdateOne, ReplaceOne # To work with LLMs: from controllers.core.ai.llm import CoreLLMController from models.core.ai.llm import LLMInput, LLMOutput # To make HTTP requests: import httpx # To work with date and time: import datetime # For asynchronous activities: import asyncio # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** CLASSES *** # ***** **** # ***************************************************************************************************************** class GmailController(MailController): # ┏┓┓ ┓┏ # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ CLIENT_NAME = "gmail" # ┏┓ # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ def __init__( self, cache: AsyncRedisCache = None, http_client: httpx.AsyncClient = None, alert_url: str = None, debug: bool = True, debug_prefix: str = "Gmail (C) | ", debug_only_errors: bool = True ): """ This is the controller specifically built for Gmail's services. It is built on top of the base mail controller. :param cache: The object to use for caching results from database calls. :param http_client: The HTTP client :param debug: Whether, or not, you would like to print debugging messages: :param debug_prefix: The prefix to print with the debugging messages. :param debug_only_errors: Whether you would like to print only error messages or all messages. :return: None. """ # Invoke the parent's constructor: super().__init__( cache = cache, alert_url = alert_url, http_client = http_client, base_filter = {"client": self.CLIENT_NAME}, debug = debug, debug_prefix = debug_prefix, debug_only_errors = debug_only_errors ) # Init a variable in a parent: self._client = self.CLIENT_NAME # ┏┓┏┓ ┓ ┏┓ ┏┓ # ┃┃┣┫┓┏╋┣┓┏┛ ┃┫ # ┗┛┛┗┗┻┗┛┗┗━•┗┛ async def get_authorization_url( self, sql_conn: AsyncMySQL, mongo_data_conn: AsyncMongo, mail_client: AsyncGmailClient, user_info: CoreUserInfoModel, inbound_data: OAuthMailAuthorizationRequestData, session_token: str ) -> OAuthMailGetAuthorizationURLResponse: """ To accept an incoming request for mail integration and provide a URL that the user can use to authorize your service to access his mail inbox. :param sql_conn: The database connection to use to perform this task. :param mongo_data_conn: The database connection to use to perform this task. :param mail_client: The instance of the third-party mail client that will be used to get the URL. :param user_info: The information about your user who is trying to use this system. :param inbound_data: The data that came in with the request (API call). :param session_token: The session token of the user. :return: A structure response with details about the URL generation process. """ # Start by assuming failure: response = OAuthMailGetAuthorizationURLResponse() # First, we create/update a record for this integration request: token_key = await self.generate_token_key( sql_conn = sql_conn, mongo_data_conn = mongo_data_conn, auth_token = CoreAuthTokenModel( serviceType = "email", client = inbound_data.mailClient, authType = "oauth", user = user_info, clientUserId = {"email": inbound_data.mailId}, status = "pending", syncFreq = inbound_data.syncFreq, ), token_notes = { "email": inbound_data.mailId, "client": inbound_data.mailClient }, display_name = inbound_data.mailId, display_picture = None, session_token = session_token ) # If generating the token key fails: if token_key is None: response.message = "Failed to generate token key." return response # Now we create the URL: response.url = await mail_client.get_authorization_url( scopes = SCOPES_GMAIL_MAIL_MANAGEMENT, state = str(token_key), access_type = "offline", approval_prompt = "force", include_granted_scopes = "true", user_email = inbound_data.mailId ) response.success = True response.message = "Please use the URL to integrate your Gmail account." # Done here: return response async def handle_authorization_callback( self, sql_conn: AsyncMySQL, mongo_data_conn: AsyncMongo, mail_client: AsyncGmailClient, request_url: str, inbound_data: dict, session_token: str = None ) -> OAuthMailHandleCallbackResponse: """ To handle the authorization callback for the mail client. The user may grant or deny authorization. :param sql_conn: The database connection to use to perform this task. :param mongo_data_conn: The database connection to use to perform this task. :param mail_client: The instance of the third-party mail client that will be used to get the URL. :param request_url: The full callback URL invoked by the third-party client. :param inbound_data: The data that came in with the request (API call). :param session_token: The session token of the user. It is expected that this will be null in all cases. :return: A structured response of the process of handling the mail callback. """ # Start by assuming failure: response = OAuthMailHandleCallbackResponse() # In case the user denied access: if inbound_data.get("error") == "access_denied": response.action = "denied" response.message = "The user denied authorization." return response # Otherwise we know that the user authorized access: else: response.action = "authorized" response.message = "The user has given authorization." # We fetch the auth-token associated with this authorization loop: auth_token = await self.get_token_from_key( mongo_data_conn = mongo_data_conn, token_key = inbound_data["state"], must_be_active = False ) if not auth_token: response.action = "unknown" response.message = "Failed to load the auth-token for this flow." return response # Generate the tokens from the callback. Google sends all the needed params in the callback as the URL's query # params. We can simply use the exact URL that was hit to generate the tokens. In Quart (and Flask) this can be # achieved by 'request.url' like this: google_tokens = await mail_client.get_authorization_tokens( redirect_url = request_url, scopes = None ) # If no tokens were generated: if not google_tokens: response.message = "Failed to get access token(s) from Gmail." return response # Try getting the user's profile from Gmail: user_profile = await mail_client.get_user_profile(tokens = google_tokens) if user_profile.success: google_tokens.email = user_profile.data["emailAddress"] google_tokens.displayName = user_profile.data["displayName"] google_tokens.displayPictureUrl = user_profile.data["displayPictureUrl"] else: response.message = "Failed to get the user's profile from Gmail." return response # We confirm if the expected email account and the one that gave authorization are the same: if auth_token.clientUserId["email"] != str(google_tokens.email): response.message = ( f"We were expecting authorization from '{auth_token.clientUserId['email']}', " f"but got authorization from '{google_tokens.email}' instead." ) return response # Now we try to create the standard set of Labels: labels = [ { "name": "TCAOFF", "textColor": "#434343", "backgroundColor": "#e7e7e7" }, { "name": "CA-Doc", "textColor": "#434343", "backgroundColor": "#e7e7e7" }, { "name": "CA-AI", "textColor": "#434343", "backgroundColor": "#e7e7e7" } ] tasks = [ mail_client.create_label( tokens = google_tokens, label_name = label["name"], label_visibility = "labelShow", message_visibility = "show", label_text_color = label["textColor"], label_background_color = label["backgroundColor"] ) for label in labels ] client_responses = await asyncio.gather(*tasks) # Add the labels to the tokens data: client_response = await mail_client.list_labels(tokens = google_tokens) google_tokens.labels = client_response.data if client_response.success else None # Now that we have passed the check, # we save the tokens to the database: auth_token.clientUserId = google_tokens.client_user_id auth_token.token = google_tokens.model_dump() auth_token.status = "active" tokens_saved = await self.set_token( sql_conn = sql_conn, mongo_data_conn = mongo_data_conn, token_key = inbound_data["state"], auth_token = auth_token, token_notes = { "email": google_tokens.email, "client": auth_token.client }, display_name = google_tokens.displayName, display_picture = google_tokens.displayPictureUrl, session_token = session_token ) # Note down the final result: if tokens_saved: response.success = True response.message = "Authorization flow completed successfully." else: response.message = "Failed to save the token(s)." # Done here: return response @AsyncLoggerContext.log_it( api_version = "1.0.0", project = constants.PROJECT_NAME, log_type = constants.MODULE_NAME, operation = "gmailTokRefresh", log_input = False, log_output = False, sensitive_keys = ["session_token"] ) async def refresh_authorization( self, sql_conn: AsyncMySQL, mongo_data_conn: AsyncMongo, mail_client: AsyncGmailClient, http_client: httpx.AsyncClient, auth_token: CoreAuthTokenModel, force_refresh: bool = False, session_token: str = None ) -> CoreAuthTokenModel: """ To refresh the third-party client's access/authorization token(s) before use. :param sql_conn: The database connection to use when storing the refreshed tokens. :param mongo_data_conn: The database connection to use when storing the refreshed tokens. :param mail_client: The connection of the third-party mail client. :param http_client: The HTTP client to use to make the token refresh request. :param auth_token: The auth-token model of the existing integration. This may get updated if a refresh is needed (or forced). :param force_refresh: Whether, or not, you would like to force a refresh even if the token hasn't expired yet. :param session_token: The session token of the user. This will be null if this method is invoked by a cron script in the background. Needed only to identify the user in case of a failure to send a timely alert. :return: The same auth-token model instance, but maybe with updated tokens. """ # Extract the client's tokens from the full token payload given by the database: google_tokens = GoogleAuthTokens(**auth_token.token) # Refresh the tokens (if/as needed): tokens_refreshed = await google_tokens.arefresh( http_client = http_client, client_id = mail_client.client_id, client_secret = mail_client.client_secret, force_refresh = force_refresh ) # If the tokens were refreshed: if tokens_refreshed: # Try getting the user's profile from Gmail: user_profile = await mail_client.get_user_profile(tokens = google_tokens) if user_profile.success: google_tokens.email = user_profile.data["emailAddress"] google_tokens.displayName = user_profile.data["displayName"] google_tokens.displayPictureUrl = user_profile.data["displayPictureUrl"] # Update the existing auth-token model: auth_token.token = google_tokens.model_dump() auth_token.lastRefreshTs = date_time.get_current_utc_date_time(as_string = True) # Try to update the record in the database: await self.set_token( sql_conn = sql_conn, mongo_data_conn = mongo_data_conn, token_key = auth_token.key, auth_token = auth_token, token_notes = { "email": auth_token.clientUserId.get("email"), "client": auth_token.client }, display_name = google_tokens.email, display_picture = google_tokens.displayPictureUrl, session_token = session_token, ) # Whether refreshed, or not, return the auth-token model: return auth_token # ┳┳┓ •┓ ┏┓ • • # ┃┃┃┏┓┓┃ ┗┓┓┏┏┳┓┏┳┓┏┓┏┓┓┓┏┓╋┓┏┓┏┓ # ┛ ┗┗┻┗┗ ┗┛┗┻┛┗┗┛┗┗┗┻┛ ┗┗┗┻┗┗┗┛┛┗ pass # ┳┳┓ •┓ ┏┓ ╹• # ┃┃┃┏┓┓┃ ┗┓┓┏┏┓┏ ┓┏┓┏┓ # ┛ ┗┗┻┗┗ ┗┛┗┫┛┗┗ ┗┛┗┗┫ # ┛ ┛ # To synchronize the mails on the third-party client's server and your server. You are effectively making a copy of # the mail on your database. async def __sync_one_mail( self, mongo_data_conn: AsyncMongo, user_info: CoreUserInfoModel, auth_token: CoreAuthTokenModel, mail_client: AsyncGmailClient, google_tokens: GoogleAuthTokens, message_id: str, llm: CoreLLMController = None, force_sync: bool = False ) -> MailSyncOneResult: """ To fetch one mail from the third-party client and store it in your database. :param mongo_data_conn: The database connection to use to store the mail's payload. :param user_info: The information about the user to whom this mail belongs. :param auth_token: The credentials to use to get the mail from the third-party client. :param mail_client: The third-party client's connection object. :param google_tokens: The mail client's tokens the way they have to be used in their connection. :param message_id: The way the third-party client recognizes the mail. :param llm: To summarize the mail. :param force_sync: If you'd like to forcefully re-sync the mail if its record already exists in your database. :return: The structured response to express how the mail fetching went. """ # Start by assuming failure: sync_result = MailSyncOneResult() # Check if the mail already exists in your database: mail_records = await self.get_message_previews( mongo_data_conn = mongo_data_conn, token_ids = [ObjectId(auth_token.authTokenId)], limit = 1, skip = 0, additional_filter = { "tokenId": ObjectId(auth_token.authTokenId), "serviceType": auth_token.serviceType, "client": auth_token.client, "clientMessageId": message_id } ) if mail_records: sync_result.success = True sync_result.isNew = False sync_result.message = ( f"Gmail message '{message_id}' already " f"sync'd on {mail_records[0].syncTs} (UTC)." ) # If the mail already exists and we have not been asked to force-sync: if not sync_result.isNew and not force_sync: return sync_result # Now that we know that we have to fetch the mail from GMail: sync_result.attempted = True client_response = await mail_client.get_message( tokens = google_tokens, message_id = message_id, return_raw = False ) # If we didn't get the mail from Gmail; if not client_response.success: sync_result.message = f"Gmail (messageId: '{message_id}'): {client_response.message}" return sync_result # HANDLE ATTACHMENTS HERE: client_response.data["payload"] = self.drop_attachments(client_response.data["payload"]) # Now we structure the message into the model: all_recipients = [] for field in ["to", "cc", "bcc"]: all_recipients += [item["email"] for item in client_response.data[field]] is_sent = False if google_tokens.email in all_recipients else True mail_message = CoreMessageModel( ts = client_response.data["ts"], syncTs = date_time.get_current_utc_date_time(as_string = False), tokenId = auth_token.authTokenId, serviceType = auth_token.serviceType, client = auth_token.client, clientMessageId = message_id, clientThreadId = client_response.data["threadId"], isSent = True if is_sent else False, isBroadcast = False, sentSuccessfully = True if is_sent else False, sender = [client_response.data["from"][0]["name"]], recipient = all_recipients, chat = None, message = client_response.data, snippet = client_response.data["subject"], aiSnippet = None, tags = ["Email", "Gmail"] ) # Invoke the LLM: try: ai_snippet = await self.summarize_mail_with_ai( mongo_data_conn = mongo_data_conn, user_info = user_info, llm = llm, message = mail_message, prompt_template = self.SENT_MAIL_SUMMARIZATION_PROMPT_TEMPLATE if is_sent else self.RECEIVED_MAIL_SUMMARIZATION_PROMPT_TEMPLATE ) mail_message.aiSnippet = ai_snippet.summary ai_json = ai_snippet.output_json mail_message.aiSnippet["output"] = ai_json["summary"] if ai_json["senderType"] is not None: mail_message.tags.append(ai_json["senderType"]) except Exception as exception: self._printer(exception) # Done here: sync_result.success = True sync_result.mailMessage = mail_message return sync_result async def sync_mails( self, sql_conn: AsyncMySQL, mongo_data_conn: AsyncMongo, mail_client: AsyncGmailClient, auth_token: CoreAuthTokenModel, user_info: CoreUserInfoModel | None, llm: CoreLLMController = None, force_sync: bool = False, start_date: datetime.datetime = None, end_date: datetime.datetime = None, max_count: int = 100, session_token: str = None ) -> MailSyncManyResults: """ To fetch mails from the third-party client and store them to your database. :param sql_conn: The database connection to use to perform this task. Needed if the auth tokens need to be refreshed or updated. :param mongo_data_conn: The database connection to use to perform this task. :param mail_client: The instance of the third-party mail client that will be used to get the URL. :param auth_token: The credentials to the account with the third-party client. :param user_info: The information about your user who is trying to use this system. Needed to note LLM token usage in the process of mail summarization. :param llm: The instance of the LLm that can be used to summarize the contents of the mail. :param force_sync: To forcefully sync a mail even if it already exists in the database. :param start_date: The starting date (inclusive) from which mails must be sync'd. :param end_date: The ending date (inclusive) till which mails must be sync'd. :param max_count: The max. no. of mails to sync. :param session_token: TO identify a user session. This will be null if a cron script invokes this method, else it will be received from the inputs of the API call. :return: The structured response to express how the mail fetching went. """ # Start by assuming failure: sync_results = MailSyncManyResults() # Refresh the access token(s) if needed: auth_token = await self.refresh_authorization( sql_conn = sql_conn, mongo_data_conn = mongo_data_conn, mail_client = mail_client, http_client = mail_client.http_client, auth_token = auth_token, force_refresh = False, session_token = session_token ) # If the user info was not given, take it from the token model: if user_info is None: user_info = auth_token.user # Extract the client's tokens from the full token model, # and check if they are valid (not expired): google_tokens = GoogleAuthTokens(**auth_token.token) if google_tokens.expired: sync_results.message = "Gmail token(s) have expired." # Let's build the query to send to Google: sub_queries = [] if start_date: sub_queries.append(start_date.strftime("after:%Y/%m/%d")) if end_date: sub_queries.append((end_date + datetime.timedelta(days = 1)).strftime("before:%Y/%m/%d")) query_string = " ".join(sub_queries) # Let's enlist all the mails that fall in the date range: client_response = await mail_client.list_messages( tokens = google_tokens, max_count = max_count, query = query_string ) # If the mail listing fails: if not client_response.success: self._printer( client_response.success, client_response.message, client_response.data, client_response.exception ) sync_results.message = f"Gmail: {client_response.message}" return sync_results # Now, for every mail in the list, we fetch the mail and note the results: messages_list = client_response.data["messages"] tasks = [ self.__sync_one_mail( mongo_data_conn = mongo_data_conn, user_info = user_info, auth_token = auth_token, mail_client = mail_client, google_tokens = google_tokens, message_id = v["id"], llm = llm, force_sync = force_sync ) for v in messages_list.values() ] individual_sync_results = await asyncio.gather(*tasks) # Now we create operations for each mail, # and maintain success/failure counters: sync_results.totalCount = len(individual_sync_results) mongo_operations = [] for result in individual_sync_results: # Maintain the result counters: if result.attempted: if result.success: sync_results.successCount += 1 else: sync_results.failureCount += 1 if result.isNew: sync_results.newCount += 1 # Create the database requests: if result.mailMessage: replacement_json = result.mailMessage.model_dump() replacement_json.pop("_id", None) mongo_operations.append( ReplaceOne( filter = { "tokenId": ObjectId(auth_token.authTokenId), "serviceType": auth_token.serviceType, "client": auth_token.client, "clientMessageId": result.mailMessage.clientMessageId }, replacement = replacement_json, upsert = True ) ) # Make the bulk operation: if mongo_operations: sync_count = await self.bulk_operate_messages( mongo_data_conn = mongo_data_conn, mongo_operations = mongo_operations ) # Apply the labels to the read messages: try: client_response = await mail_client.modify_messages( tokens = google_tokens, message_ids = [v["id"] for v in messages_list.values()], add_label_ids = [google_tokens.labels.get("TCAOFF", {}).get("id")] ) except Exception as exception: pass # Done here: sync_results.message = f"{sync_results.successCount}/{sync_results.totalCount} mail(s) sync'd from Gmail." return sync_results # ┳┳┓ •┓ ┓ • • # ┃┃┃┏┓┓┃ ┃ ┓┏╋┓┏┓┏┓ # ┛ ┗┗┻┗┗ ┗┛┗┛┗┗┛┗┗┫ # ┛ # Use these to show your users their mails once the mails are on your server. This would include activities like # listing mails, showing full mails, showing mail trails, etc. pass # ┳┳┓ •┓ ┏┓ ┓• # ┃┃┃┏┓┓┃ ┗┓┏┓┏┓┏┫┓┏┓┏┓ # ┛ ┗┗┻┗┗ ┗┛┗ ┛┗┗┻┗┛┗┗┫ # ┛ async def send_mail( self, sql_conn: AsyncMySQL, mongo_data_conn: AsyncMongo, mail_client: AsyncGmailClient, mail_message: GmailMessage, auth_token: CoreAuthTokenModel, client_thread_id: str | None, user_info: CoreUserInfoModel | None, llm: CoreLLMController = None, session_token: str = None ) -> MailSendOneResult: # Start by assuming failure: send_result = MailSendOneResult() now = date_time.get_current_utc_date_time(as_string = False) # Refresh the access token(s) if needed: auth_token = await self.refresh_authorization( sql_conn = sql_conn, mongo_data_conn = mongo_data_conn, mail_client = mail_client, http_client = mail_client.http_client, auth_token = auth_token, force_refresh = False, session_token = session_token ) # If the user info was not given, take it from the token model: if user_info is None: user_info = auth_token.user # Extract the client's tokens from the full token model, # and check if they are valid (not expired): google_tokens = GoogleAuthTokens(**auth_token.token) if google_tokens.expired: send_result.message = "Gmail token(s) have expired." return send_result # Try sending the message: client_response = await mail_client.send_message( tokens = google_tokens, message = mail_message ) # If the attempt failed: if not client_response.success: send_result.message = f"Gmail: {client_response.message}" return send_result # Parse the mail to save it to the database: mail_json = mail_parser.parse(mail_message.get_raw_message(as_base64 = False)) mail_json["labels"] = client_response.data.get("labelIds", []) mail_json["messageId"] = client_response.data["id"] mail_json["threadId"] = client_response.data["threadId"] mail_json["historyId"] = client_response.data.get("historyId") mail_json["snippet"] = client_response.data.get("snippet", mail_message.subject) mail_json["sizeEstimate"] = client_response.data.get("sizeEstimate") # HANDLE ATTACHMENTS HERE: mail_json["payload"] = self.drop_attachments(mail_json["payload"]) # Now we structure the message into the model: all_recipients = [] for field in ["to", "cc", "bcc"]: all_recipients += [item["email"] for item in mail_json[field]] mail_message = CoreMessageModel( ts = mail_json["ts"] or now, syncTs = now, tokenId = auth_token.authTokenId, serviceType = auth_token.serviceType, client = auth_token.client, clientMessageId = client_response.data["id"], clientThreadId = client_response.data["threadId"], isSent = True, isBroadcast = False, sentSuccessfully = True, sender = [mail_json["from"][0]["name"]], recipient = all_recipients, chat = None, message = mail_json, snippet = mail_json["subject"], aiSnippet = None, tags = ["Email", "Gmail", "Sent"] ) # Invoke the LLM: try: ai_snippet = await self.summarize_mail_with_ai( mongo_data_conn = mongo_data_conn, user_info = user_info, llm = llm, message = mail_message, prompt_template = self.SENT_MAIL_SUMMARIZATION_PROMPT_TEMPLATE ) mail_message.aiSnippet = ai_snippet.summary except Exception as exception: self._printer(exception) # Save the message to the database: success = await mongo_data_conn.replace_one( collection = self.MESSAGES_COLLECTION, filter = { "tokenId": ObjectId(auth_token.authTokenId), "serviceType": auth_token.serviceType, "client": auth_token.client, "clientMessageId": mail_message.clientMessageId }, replacement = mail_message.model_dump(), upsert = True ) # Done here: send_result.success = True send_result.message = f"Mail sent successfully." return send_result # ┳┳┓ •┓ ┳┳ ┓ • # ┃┃┃┏┓┓┃ ┃┃┏┓┏┫┏┓╋┓┏┓┏┓ # ┛ ┗┗┻┗┗ ┗┛┣┛┗┻┗┻┗┗┛┗┗┫ # ┛ ┛ # We cannot modify the mails themselves, but we can set/unset tags on them for internal referencing and filtering. # This will help the users organize their inboxes well. pass # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": pass