Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email util integration generalising #490

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e3e245a
partial working draft fix for gmail utils issues
mohanqxf2 Jul 24, 2024
aea6ba1
working draft for gmail uitls
mohanqxf2 Jul 25, 2024
54e06b1
fixed parse parse subject methods in Message file
mohanqxf2 Jul 25, 2024
3fc3555
working commit for use mailbox and fetch methods
mohanqxf2 Jul 25, 2024
54f99aa
fix draft for fetch multiple messages
mohanqxf2 Jul 29, 2024
7b9aeaa
fix tuple to string in fetch multiple messages
mohanqxf2 Jul 29, 2024
232c000
tests/test_gmail_utils.py
mohanqxf2 Jul 29, 2024
1305e0e
working fix for fetching multiple messages
mohanqxf2 Jul 29, 2024
6b3455d
updated credentials to fetch from env conf file
mohanqxf2 Jul 29, 2024
bb08aa7
updated with adding pytest to the script
mohanqxf2 Aug 8, 2024
e0b8f0c
code cleanup
mohanqxf2 Aug 8, 2024
ebe1d1d
code cleanup
mohanqxf2 Aug 8, 2024
34a2221
working fix for fetchMultipleMessages wrt all the imap hosts
mohanqxf2 Aug 20, 2024
8ce5a92
work in progress for fetch threads
mohanqxf2 Aug 21, 2024
248cdd1
working fix for fetching thread subjects in chronological order
mohanqxf2 Aug 23, 2024
4b02cb5
fix codecy for mailbox
mohanqxf2 Aug 23, 2024
62545fe
working draft for featch threads for 0 or more than 1
mohanqxf2 Aug 28, 2024
1588c14
working commit for fetch threads for outlook
mohanqxf2 Aug 30, 2024
6ebc87e
code clean up and working fetch threads from all host
mohanqxf2 Sep 13, 2024
121ee69
change the nameing conventions for class and variables to genaralise …
mohanqxf2 Sep 18, 2024
0a8b083
rebase with latest master
mohanqxf2 Oct 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
__license__ = 'Apache 2.0'
__copyright__ = 'Copyright 2013 Charlie Guo'

from .gmail import Gmail
from .email import Email
from .mailbox import Mailbox
from .message import Message
from .exceptions import GmailException, ConnectionError, AuthenticationError
from .exceptions import EmailException, ConnectionError, AuthenticationError
from .utils import login, authenticate

Original file line number Diff line number Diff line change
@@ -1,49 +1,53 @@
"""
This module defines the `Gmail` class, which provides methods to interact with a Gmail account
via IMAP. The class includes functionalities to connect to the Gmail server, login using
credentials, and manage various mailboxes. Also, has functions for fetching mailboxes,
selecting a specific mailbox, searching for and retrieving emails
based on different criteria, managing labels, and handling authentication.
"""

from __future__ import absolute_import
import os
import sys
import re
import imaplib
from integrations.reporting_channels.gmail.mailbox import Mailbox
from integrations.reporting_channels.gmail.utf import encode as encode_utf7, decode as decode_utf7
from integrations.reporting_channels.gmail.exceptions import *
import logging
from email.header import decode_header
from dotenv import load_dotenv
from integrations.reporting_channels.email.mailbox import Mailbox
from integrations.reporting_channels.email.utf import encode as encode_utf7, decode as decode_utf7
from integrations.reporting_channels.email.exceptions import *
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
load_dotenv()


logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

class Gmail():
"Class interact with Gmail using IMAP"
# GMail IMAP defaults
GMAIL_IMAP_HOST = 'imap.gmail.com'
GMAIL_IMAP_PORT = 993
class Email():
# EMail IMAP defaults
IMAP_HOST = os.getenv('imaphost')
IMAP_PORT = 993

# GMail SMTP defaults
GMAIL_SMTP_HOST = "smtp.gmail.com"
GMAIL_SMTP_PORT = 587
# EMail SMTP defaults
EMAIL_SMTP_HOST = os.getenv('smtp_ssl_host')
EMAIL_SMTP_PORT = os.getenv('smtp_ssl_port')

def __init__(self):
self.username = None
self.password = None
self.access_token = None

self.imap = None
self.smtp = None
self.logged_in = False
self.mailboxes = {}
self.current_mailbox = None


# self.connect()


def connect(self, raise_errors=True):
"Establishes an IMAP connection to the Gmail server."
# try:
# self.imap = imaplib.IMAP4_SSL(self.GMAIL_IMAP_HOST, self.GMAIL_IMAP_PORT)
# except socket.error:
# if raise_errors:
# raise Exception('Connection failure.')
# self.imap = None

self.imap = imaplib.IMAP4_SSL(self.GMAIL_IMAP_HOST, self.GMAIL_IMAP_PORT)
self.imap = imaplib.IMAP4_SSL(self.IMAP_HOST, self.IMAP_PORT)

# self.smtp = smtplib.SMTP(self.server,self.port)
# self.smtp.set_debuglevel(self.debug)
Expand All @@ -53,8 +57,9 @@ def connect(self, raise_errors=True):

return self.imap


# Add fetch_mailboxes method in the Email class
def fetch_mailboxes(self):
"Retrieves and stores the list of mailboxes available in the Gmail account."
response, mailbox_list = self.imap.list()
if response == 'OK':
mailbox_list = [item.decode('utf-8') if isinstance(item, bytes) else item for item in mailbox_list]
Expand All @@ -67,26 +72,24 @@ def fetch_mailboxes(self):
else:
raise Exception("Failed to fetch mailboxes.")


def use_mailbox(self, mailbox):
"Selects a specific mailbox for further operations."
if mailbox:
self.imap.select(mailbox)
self.current_mailbox = mailbox
return Mailbox(self, mailbox)


def mailbox(self, mailbox_name):
"Returns a Mailbox object for the given mailbox name."
if mailbox_name not in self.mailboxes:
mailbox_name = encode_utf7(mailbox_name)
mailbox = self.mailboxes.get(mailbox_name)

if mailbox and not self.current_mailbox == mailbox_name:
self.use_mailbox(mailbox_name)

return mailbox

def create_mailbox(self, mailbox_name):
"Creates a new mailbox with the given name if it does not already exist."
mailbox = self.mailboxes.get(mailbox_name)
if not mailbox:
self.imap.create(mailbox_name)
Expand All @@ -96,19 +99,20 @@ def create_mailbox(self, mailbox_name):
return mailbox

def delete_mailbox(self, mailbox_name):
"Deletes the specified mailbox and removes it from the cache."
mailbox = self.mailboxes.get(mailbox_name)
if mailbox:
self.imap.delete(mailbox_name)
del self.mailboxes[mailbox_name]



def login(self, username, password):
"Login to Gmail using the provided username and password. "
self.username = username
self.password = password

if not self.imap:
self.connect()

try:
imap_login = self.imap.login(self.username, self.password)
self.logged_in = (imap_login and imap_login[0] == 'OK')
Expand All @@ -117,15 +121,18 @@ def login(self, username, password):
except imaplib.IMAP4.error:
raise AuthenticationError


# smtp_login(username, password)

return self.logged_in

def authenticate(self, username, access_token):
"Login to Gmail using OAuth2 with the provided username and access token."
self.username = username
self.access_token = access_token

if not self.imap:
self.connect()

try:
auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token)
imap_auth = self.imap.authenticate('XOAUTH2', lambda x: auth_string)
Expand All @@ -138,32 +145,29 @@ def authenticate(self, username, access_token):
return self.logged_in

def logout(self):
"Logout from the Gmail account and closes the IMAP connection."
self.imap.logout()
self.logged_in = False


def label(self, label_name):
"Retrieves a Mailbox object for the specified label (mailbox)."
return self.mailbox(label_name)

def find(self, mailbox_name="[Gmail]/All Mail", **kwargs):
"Searches and returns emails based on the provided search criteria."
box = self.mailbox(mailbox_name)
return box.mail(**kwargs)


def copy(self, uid, to_mailbox, from_mailbox=None):
"Copies an email with the given UID from one mailbox to another."
if from_mailbox:
self.use_mailbox(from_mailbox)
self.imap.uid('COPY', uid, to_mailbox)

def fetch_multiple_messages(self, messages):
"Fetches and parses multiple messages given a dictionary of `Message` objects."
if not isinstance(messages, dict):
raise Exception('Messages must be a dictionary')

fetch_str = ','.join(messages.keys())
response, results = self.imap.uid('FETCH', fetch_str, '(BODY.PEEK[] FLAGS X-GM-THRID X-GM-MSGID X-GM-LABELS)')
response, results = self.imap.uid('FETCH', fetch_str, '(UID BODY.PEEK[] FLAGS)')

for raw_message in results:
if isinstance(raw_message, tuple):
Expand All @@ -172,39 +176,40 @@ def fetch_multiple_messages(self, messages):
uid = uid_match.group(1).decode('utf-8')
if uid in messages:
messages[uid].parse(raw_message)
else:
logging.warning(f'UID {uid} not found in messages dictionary')
else:
logging.warning('UID not found in raw message')
elif isinstance(raw_message, bytes):
continue
else:
logging.warning('Invalid raw message format')

return messages

def labels(self, require_unicode=False):
"Returns a list of all available mailbox names."
keys = self.mailboxes.keys()
if require_unicode:
keys = [decode_utf7(key) for key in keys]
return keys

def inbox(self):
"Returns a `Mailbox` object for the Inbox."
return self.mailbox("INBOX")

def spam(self):
"Returns a `Mailbox` object for the Spam."
return self.mailbox("[Gmail]/Spam")

def starred(self):
"Returns a `Mailbox` object for the starred folder."
return self.mailbox("[Gmail]/Starred")

def all_mail(self):
"Returns a `Mailbox` object for the All mail."
return self.mailbox("[Gmail]/All Mail")

def sent_mail(self):
"Returns a `Mailbox` object for the Sent mail."
return self.mailbox("[Gmail]/Sent Mail")

def important(self):
"Returns a `Mailbox` object for the Important."
return self.mailbox("[Gmail]/Important")

def mail_domain(self):
"Returns the domain part of the logged-in email address"
return self.username.split('@')[-1]
return self.username.split('@')[-1]
23 changes: 23 additions & 0 deletions integrations/reporting_channels/email/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-

"""
email.exceptions
~~~~~~~~~~~~~~~~~~~

This module contains the set of Emails' exceptions.

"""


class EmailException(RuntimeError):
"""There was an ambiguous exception that occurred while handling your
request."""

class ConnectionError(EmailException):
"""A Connection error occurred."""

class AuthenticationError(EmailException):
"""Email Authentication failed."""

class Timeout(EmailException):
"""The request timed out."""
105 changes: 105 additions & 0 deletions integrations/reporting_channels/email/mailbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import re
from .message import Message
from .utf import encode as encode_utf7, decode as decode_utf7

class Mailbox():
def __init__(self, email, name="INBOX"):
self.name = name
self.email = email
self.date_format = "%d-%b-%Y"
self.messages = {}

@property
def external_name(self):
if "external_name" not in vars(self):
vars(self)["external_name"] = encode_utf7(self.name)
return vars(self)["external_name"]

@external_name.setter
def external_name(self, value):
if "external_name" in vars(self):
del vars(self)["external_name"]
self.name = decode_utf7(value)

def mail(self, prefetch=False, **kwargs):
search = ['ALL']

kwargs.get('read') and search.append('SEEN')
kwargs.get('unread') and search.append('UNSEEN')

kwargs.get('starred') and search.append('FLAGGED')
kwargs.get('unstarred') and search.append('UNFLAGGED')

kwargs.get('deleted') and search.append('DELETED')
kwargs.get('undeleted') and search.append('UNDELETED')

kwargs.get('draft') and search.append('DRAFT')
kwargs.get('undraft') and search.append('UNDRAFT')

kwargs.get('before') and search.extend(['BEFORE', kwargs.get('before').strftime(self.date_format)])
kwargs.get('after') and search.extend(['SINCE', kwargs.get('after').strftime(self.date_format)])
kwargs.get('on') and search.extend(['ON', kwargs.get('on').strftime(self.date_format)])

kwargs.get('header') and search.extend(['HEADER', kwargs.get('header')[0], kwargs.get('header')[1]])

kwargs.get('sender') and search.extend(['FROM', kwargs.get('sender')])
kwargs.get('fr') and search.extend(['FROM', kwargs.get('fr')])
kwargs.get('to') and search.extend(['TO', kwargs.get('to')])
kwargs.get('cc') and search.extend(['CC', kwargs.get('cc')])

kwargs.get('subject') and search.extend(['SUBJECT', kwargs.get('subject')])
kwargs.get('body') and search.extend(['BODY', kwargs.get('body')])

kwargs.get('label') and search.extend(['X-GM-LABELS', kwargs.get('label')])
kwargs.get('attachment') and search.extend(['HAS', 'attachment'])

kwargs.get('query') and search.extend([kwargs.get('query')])

emails = []
search_criteria = ' '.join(search).encode('utf-8') # Ensure the search criteria are byte strings

response, data = self.email.imap.uid('SEARCH', None, search_criteria)
if response == 'OK':
uids = filter(None, data[0].split(b' ')) # filter out empty strings

for uid in uids:
if not self.messages.get(uid):
self.messages[uid] = Message(self, uid)
emails.append(self.messages[uid])

if prefetch and emails:
messages_dict = {}
for email in emails:
messages_dict[email.uid] = email
self.messages.update(self.email.fetch_multiple_messages(messages_dict))

return emails

# WORK IN PROGRESS. NOT FOR ACTUAL USE
def threads(self, prefetch=False, **kwargs):
emails = []
response, data = self.email.imap.uid('SEARCH', None, 'ALL'.encode('utf-8'))
if response == 'OK':
uids = data[0].split(b' ')

for uid in uids:
if not self.messages.get(uid):
self.messages[uid] = Message(self, uid)
emails.append(self.messages[uid])

if prefetch:
fetch_str = ','.join(uids).encode('utf-8')
response, results = self.email.imap.uid('FETCH', fetch_str, '(BODY.PEEK[] FLAGS X-GM-THRID X-GM-MSGID X-GM-LABELS)')
for index in range(len(results) - 1):
raw_message = results[index]
if re.search(rb'UID (\d+)', raw_message[0]):
uid = re.search(rb'UID (\d+)', raw_message[0]).groups(1)[0]
self.messages[uid].parse(raw_message)

return emails

def count(self, **kwargs):
return len(self.mail(**kwargs))

def cached_messages(self):
return self.messages
Loading