├── .gitignore ├── LICENSE ├── README.md ├── gmail ├── __init__.py ├── exceptions.py ├── gmail.py ├── mailbox.py ├── message.py ├── utf.py └── utils.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | *.pyc 18 | 19 | ## PROJECT::SPECIFIC 20 | *.rdb 21 | 22 | ## Test Account details 23 | spec/account.yml 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Charlie Guo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GMail for Python 2 | 3 | A Pythonic interface to Google's GMail, with all the tools you'll need. Search, 4 | read and send multipart emails, archive, mark as read/unread, delete emails, 5 | and manage labels. 6 | 7 | __This library is still under development, so please forgive some of the rough edges__ 8 | 9 | Heavily inspired by [Kriss "nu7hatch" Kowalik's GMail for Ruby library](https://github.com/nu7hatch/gmail) 10 | 11 | ## Author 12 | 13 | * [Charlie Guo](https://github.com/charlierguo) 14 | 15 | ## Installation 16 | 17 | For now, installation is manual (`pip` support not yet implemented) and the only requirement is to use Python 2 (2.7+ to be precise): 18 | 19 | git clone git://github.com/charlierguo/gmail.git 20 | 21 | ## Features 22 | 23 | * Search emails 24 | * Read emails 25 | * Emails: label, archive, delete, mark as read/unread/spam, star 26 | * Manage labels 27 | 28 | ## Basic usage 29 | 30 | To start, import the `gmail` library. 31 | 32 | import gmail 33 | 34 | ### Authenticating gmail sessions 35 | 36 | To easily get up and running: 37 | 38 | import gmail 39 | 40 | g = gmail.login(username, password) 41 | 42 | Which will automatically log you into a GMail account. 43 | This is actually a shortcut for creating a new Gmail object: 44 | 45 | from gmail import Gmail 46 | 47 | g = Gmail() 48 | g.login(username, password) 49 | # play with your gmail... 50 | g.logout() 51 | 52 | You can also check if you are logged in at any time: 53 | 54 | g = gmail.login(username, password) 55 | g.logged_in # Should be True, AuthenticationError if login fails 56 | 57 | ### OAuth authentication 58 | 59 | If you have already received an [OAuth2 access token from Google](https://developers.google.com/accounts/docs/OAuth2) for a given user, you can easily log the user in. (Because OAuth 1.0 usage was deprecated in April 2012, this library does not currently support its usage) 60 | 61 | gmail = gmail.authenticate(username, access_token) 62 | 63 | ### Filtering emails 64 | 65 | Get all messages in your inbox: 66 | 67 | g.inbox().mail() 68 | 69 | Get messages that fit some criteria: 70 | 71 | g.inbox().mail(after=datetime.date(2013, 6, 18), before=datetime.date(2013, 8, 3)) 72 | g.inbox().mail(on=datetime.date(2009, 1, 1) 73 | g.inbox().mail(sender="myfriend@gmail.com") # "from" is reserved, use "fr" or "sender" 74 | g.inbox().mail(to="directlytome@gmail.com") 75 | 76 | Combine flags and options: 77 | 78 | g.inbox().mail(unread=True, sender="myboss@gmail.com") 79 | 80 | Browsing labeled emails is similar to working with your inbox. 81 | 82 | g.mailbox('Urgent').mail() 83 | 84 | Every message in a conversation/thread will come as a separate message. 85 | 86 | g.inbox().mail(unread=True, before=datetime.date(2013, 8, 3) sender="myboss@gmail.com") 87 | 88 | ### Working with emails 89 | 90 | __Important: calls to `mail()` will return a list of empty email messages (with unique IDs). To work with labels, headers, subjects, and bodies, call `fetch()` on an individual message. You can call mail with `prefetch=True`, which will fetch the bodies automatically.__ 91 | 92 | unread = g.inbox().mail(unread=True) 93 | print unread[0].body 94 | # None 95 | 96 | unread[0].fetch() 97 | print unread[0].body 98 | # Dear ..., 99 | 100 | Mark news past a certain date as read and archive it: 101 | 102 | emails = g.inbox().mail(before=datetime.date(2013, 4, 18), sender="news@nbcnews.com") 103 | for email in emails: 104 | email.read() # can also unread(), delete(), spam(), or star() 105 | email.archive() 106 | 107 | Delete all emails from a certain person: 108 | 109 | emails = g.inbox().mail(sender="junkmail@gmail.com") 110 | for email in emails: 111 | email.delete() 112 | 113 | You can use also `label` method instead of `mailbox`: 114 | 115 | g.label("Faxes").mail() 116 | 117 | Add a label to a message: 118 | 119 | email.add_label("Faxes") 120 | 121 | Download message attachments: 122 | 123 | for attachment in email.attachments: 124 | print 'Saving attachment: ' + attachment.name 125 | print 'Size: ' + str(attachment.size) + ' KB' 126 | attachment.save('attachments/' + attachment.name) 127 | 128 | There is also few shortcuts to mark messages quickly: 129 | 130 | email.read() 131 | email.unread() 132 | email.spam() 133 | email.star() 134 | email.unstar() 135 | 136 | ### Roadmap 137 | * Write tests 138 | * Better label support 139 | * Moving between labels/mailboxes 140 | * Intuitive thread fetching & manipulation 141 | * Sending mail via Google's SMTP servers (for now, check out https://github.com/paulchakravarti/gmail-sender) 142 | 143 | ## Copyright 144 | 145 | * Copyright (c) 2013 Charlie Guo 146 | 147 | See LICENSE for details. 148 | 149 | -------------------------------------------------------------------------------- /gmail/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | """ 4 | 5 | GMail! Woo! 6 | 7 | """ 8 | 9 | __title__ = 'gmail' 10 | __version__ = '0.1' 11 | __author__ = 'Charlie Guo' 12 | __build__ = 0x0001 13 | __license__ = 'Apache 2.0' 14 | __copyright__ = 'Copyright 2013 Charlie Guo' 15 | 16 | from .gmail import Gmail 17 | from .mailbox import Mailbox 18 | from .message import Message 19 | from .exceptions import GmailException, ConnectionError, AuthenticationError 20 | from .utils import login, authenticate 21 | 22 | -------------------------------------------------------------------------------- /gmail/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | gmail.exceptions 5 | ~~~~~~~~~~~~~~~~~~~ 6 | 7 | This module contains the set of Gmails' exceptions. 8 | 9 | """ 10 | 11 | 12 | class GmailException(RuntimeError): 13 | """There was an ambiguous exception that occurred while handling your 14 | request.""" 15 | 16 | class ConnectionError(GmailException): 17 | """A Connection error occurred.""" 18 | 19 | class AuthenticationError(GmailException): 20 | """Gmail Authentication failed.""" 21 | 22 | class Timeout(GmailException): 23 | """The request timed out.""" 24 | -------------------------------------------------------------------------------- /gmail/gmail.py: -------------------------------------------------------------------------------- 1 | import re 2 | import imaplib 3 | 4 | from mailbox import Mailbox 5 | from utf import encode as encode_utf7, decode as decode_utf7 6 | from exceptions import * 7 | 8 | class Gmail(): 9 | # GMail IMAP defaults 10 | GMAIL_IMAP_HOST = 'imap.gmail.com' 11 | GMAIL_IMAP_PORT = 993 12 | 13 | # GMail SMTP defaults 14 | # TODO: implement SMTP functions 15 | GMAIL_SMTP_HOST = "smtp.gmail.com" 16 | GMAIL_SMTP_PORT = 587 17 | 18 | def __init__(self): 19 | self.username = None 20 | self.password = None 21 | self.access_token = None 22 | 23 | self.imap = None 24 | self.smtp = None 25 | self.logged_in = False 26 | self.mailboxes = {} 27 | self.current_mailbox = None 28 | 29 | 30 | # self.connect() 31 | 32 | 33 | def connect(self, raise_errors=True): 34 | # try: 35 | # self.imap = imaplib.IMAP4_SSL(self.GMAIL_IMAP_HOST, self.GMAIL_IMAP_PORT) 36 | # except socket.error: 37 | # if raise_errors: 38 | # raise Exception('Connection failure.') 39 | # self.imap = None 40 | 41 | self.imap = imaplib.IMAP4_SSL(self.GMAIL_IMAP_HOST, self.GMAIL_IMAP_PORT) 42 | 43 | # self.smtp = smtplib.SMTP(self.server,self.port) 44 | # self.smtp.set_debuglevel(self.debug) 45 | # self.smtp.ehlo() 46 | # self.smtp.starttls() 47 | # self.smtp.ehlo() 48 | 49 | return self.imap 50 | 51 | 52 | def fetch_mailboxes(self): 53 | response, mailbox_list = self.imap.list() 54 | if response == 'OK': 55 | for mailbox in mailbox_list: 56 | mailbox_name = mailbox.split('"/"')[-1].replace('"', '').strip() 57 | mailbox = Mailbox(self) 58 | mailbox.external_name = mailbox_name 59 | self.mailboxes[mailbox_name] = mailbox 60 | 61 | def use_mailbox(self, mailbox): 62 | if mailbox: 63 | self.imap.select(mailbox) 64 | self.current_mailbox = mailbox 65 | 66 | def mailbox(self, mailbox_name): 67 | if mailbox_name not in self.mailboxes: 68 | mailbox_name = encode_utf7(mailbox_name) 69 | mailbox = self.mailboxes.get(mailbox_name) 70 | 71 | if mailbox and not self.current_mailbox == mailbox_name: 72 | self.use_mailbox(mailbox_name) 73 | 74 | return mailbox 75 | 76 | def create_mailbox(self, mailbox_name): 77 | mailbox = self.mailboxes.get(mailbox_name) 78 | if not mailbox: 79 | self.imap.create(mailbox_name) 80 | mailbox = Mailbox(self, mailbox_name) 81 | self.mailboxes[mailbox_name] = mailbox 82 | 83 | return mailbox 84 | 85 | def delete_mailbox(self, mailbox_name): 86 | mailbox = self.mailboxes.get(mailbox_name) 87 | if mailbox: 88 | self.imap.delete(mailbox_name) 89 | del self.mailboxes[mailbox_name] 90 | 91 | 92 | 93 | def login(self, username, password): 94 | self.username = username 95 | self.password = password 96 | 97 | if not self.imap: 98 | self.connect() 99 | 100 | try: 101 | imap_login = self.imap.login(self.username, self.password) 102 | self.logged_in = (imap_login and imap_login[0] == 'OK') 103 | if self.logged_in: 104 | self.fetch_mailboxes() 105 | except imaplib.IMAP4.error: 106 | raise AuthenticationError 107 | 108 | 109 | # smtp_login(username, password) 110 | 111 | return self.logged_in 112 | 113 | def authenticate(self, username, access_token): 114 | self.username = username 115 | self.access_token = access_token 116 | 117 | if not self.imap: 118 | self.connect() 119 | 120 | try: 121 | auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token) 122 | imap_auth = self.imap.authenticate('XOAUTH2', lambda x: auth_string) 123 | self.logged_in = (imap_auth and imap_auth[0] == 'OK') 124 | if self.logged_in: 125 | self.fetch_mailboxes() 126 | except imaplib.IMAP4.error: 127 | raise AuthenticationError 128 | 129 | return self.logged_in 130 | 131 | def logout(self): 132 | self.imap.logout() 133 | self.logged_in = False 134 | 135 | 136 | def label(self, label_name): 137 | return self.mailbox(label_name) 138 | 139 | def find(self, mailbox_name="[Gmail]/All Mail", **kwargs): 140 | box = self.mailbox(mailbox_name) 141 | return box.mail(**kwargs) 142 | 143 | 144 | def copy(self, uid, to_mailbox, from_mailbox=None): 145 | if from_mailbox: 146 | self.use_mailbox(from_mailbox) 147 | self.imap.uid('COPY', uid, to_mailbox) 148 | 149 | def fetch_multiple_messages(self, messages): 150 | fetch_str = ','.join(messages.keys()) 151 | response, results = self.imap.uid('FETCH', fetch_str, '(BODY.PEEK[] FLAGS X-GM-THRID X-GM-MSGID X-GM-LABELS)') 152 | for index in xrange(len(results) - 1): 153 | raw_message = results[index] 154 | if re.search(r'UID (\d+)', raw_message[0]): 155 | uid = re.search(r'UID (\d+)', raw_message[0]).groups(1)[0] 156 | messages[uid].parse(raw_message) 157 | 158 | return messages 159 | 160 | 161 | def labels(self, require_unicode=False): 162 | keys = self.mailboxes.keys() 163 | if require_unicode: 164 | keys = [decode_utf7(key) for key in keys] 165 | return keys 166 | 167 | def inbox(self): 168 | return self.mailbox("INBOX") 169 | 170 | def spam(self): 171 | return self.mailbox("[Gmail]/Spam") 172 | 173 | def starred(self): 174 | return self.mailbox("[Gmail]/Starred") 175 | 176 | def all_mail(self): 177 | return self.mailbox("[Gmail]/All Mail") 178 | 179 | def sent_mail(self): 180 | return self.mailbox("[Gmail]/Sent Mail") 181 | 182 | def important(self): 183 | return self.mailbox("[Gmail]/Important") 184 | 185 | def mail_domain(self): 186 | return self.username.split('@')[-1] 187 | -------------------------------------------------------------------------------- /gmail/mailbox.py: -------------------------------------------------------------------------------- 1 | from message import Message 2 | from utf import encode as encode_utf7, decode as decode_utf7 3 | 4 | 5 | class Mailbox(): 6 | 7 | def __init__(self, gmail, name="INBOX"): 8 | self.name = name 9 | self.gmail = gmail 10 | self.date_format = "%d-%b-%Y" 11 | self.messages = {} 12 | 13 | @property 14 | def external_name(self): 15 | if "external_name" not in vars(self): 16 | vars(self)["external_name"] = encode_utf7(self.name) 17 | return vars(self)["external_name"] 18 | 19 | @external_name.setter 20 | def external_name(self, value): 21 | if "external_name" in vars(self): 22 | del vars(self)["external_name"] 23 | self.name = decode_utf7(value) 24 | 25 | def mail(self, prefetch=False, **kwargs): 26 | search = ['ALL'] 27 | 28 | kwargs.get('read') and search.append('SEEN') 29 | kwargs.get('unread') and search.append('UNSEEN') 30 | 31 | kwargs.get('starred') and search.append('FLAGGED') 32 | kwargs.get('unstarred') and search.append('UNFLAGGED') 33 | 34 | kwargs.get('deleted') and search.append('DELETED') 35 | kwargs.get('undeleted') and search.append('UNDELETED') 36 | 37 | kwargs.get('draft') and search.append('DRAFT') 38 | kwargs.get('undraft') and search.append('UNDRAFT') 39 | 40 | kwargs.get('before') and search.extend(['BEFORE', kwargs.get('before').strftime(self.date_format)]) 41 | kwargs.get('after') and search.extend(['SINCE', kwargs.get('after').strftime(self.date_format)]) 42 | kwargs.get('on') and search.extend(['ON', kwargs.get('on').strftime(self.date_format)]) 43 | 44 | kwargs.get('header') and search.extend(['HEADER', kwargs.get('header')[0], kwargs.get('header')[1]]) 45 | 46 | kwargs.get('sender') and search.extend(['FROM', kwargs.get('sender')]) 47 | kwargs.get('fr') and search.extend(['FROM', kwargs.get('fr')]) 48 | kwargs.get('to') and search.extend(['TO', kwargs.get('to')]) 49 | kwargs.get('cc') and search.extend(['CC', kwargs.get('cc')]) 50 | 51 | kwargs.get('subject') and search.extend(['SUBJECT', kwargs.get('subject')]) 52 | kwargs.get('body') and search.extend(['BODY', kwargs.get('body')]) 53 | 54 | kwargs.get('label') and search.extend(['X-GM-LABELS', kwargs.get('label')]) 55 | kwargs.get('attachment') and search.extend(['HAS', 'attachment']) 56 | 57 | kwargs.get('query') and search.extend([kwargs.get('query')]) 58 | 59 | emails = [] 60 | # print search 61 | response, data = self.gmail.imap.uid('SEARCH', *search) 62 | if response == 'OK': 63 | uids = filter(None, data[0].split(' ')) # filter out empty strings 64 | 65 | for uid in uids: 66 | if not self.messages.get(uid): 67 | self.messages[uid] = Message(self, uid) 68 | emails.append(self.messages[uid]) 69 | 70 | if prefetch and emails: 71 | messages_dict = {} 72 | for email in emails: 73 | messages_dict[email.uid] = email 74 | self.messages.update(self.gmail.fetch_multiple_messages(messages_dict)) 75 | 76 | return emails 77 | 78 | # WORK IN PROGRESS. NOT FOR ACTUAL USE 79 | def threads(self, prefetch=False, **kwargs): 80 | emails = [] 81 | response, data = self.gmail.imap.uid('SEARCH', 'ALL') 82 | if response == 'OK': 83 | uids = data[0].split(' ') 84 | 85 | 86 | for uid in uids: 87 | if not self.messages.get(uid): 88 | self.messages[uid] = Message(self, uid) 89 | emails.append(self.messages[uid]) 90 | 91 | if prefetch: 92 | fetch_str = ','.join(uids) 93 | response, results = self.gmail.imap.uid('FETCH', fetch_str, '(BODY.PEEK[] FLAGS X-GM-THRID X-GM-MSGID X-GM-LABELS)') 94 | for index in xrange(len(results) - 1): 95 | raw_message = results[index] 96 | if re.search(r'UID (\d+)', raw_message[0]): 97 | uid = re.search(r'UID (\d+)', raw_message[0]).groups(1)[0] 98 | self.messages[uid].parse(raw_message) 99 | 100 | return emails 101 | 102 | def count(self, **kwargs): 103 | return len(self.mail(**kwargs)) 104 | 105 | def cached_messages(self): 106 | return self.messages 107 | -------------------------------------------------------------------------------- /gmail/message.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import email 3 | import re 4 | import time 5 | import os 6 | from email.header import decode_header, make_header 7 | from imaplib import ParseFlags 8 | 9 | class Message(): 10 | 11 | 12 | def __init__(self, mailbox, uid): 13 | self.uid = uid 14 | self.mailbox = mailbox 15 | self.gmail = mailbox.gmail if mailbox else None 16 | 17 | self.message = None 18 | self.headers = {} 19 | 20 | self.subject = None 21 | self.body = None 22 | self.html = None 23 | 24 | self.to = None 25 | self.fr = None 26 | self.cc = None 27 | self.delivered_to = None 28 | 29 | self.sent_at = None 30 | 31 | self.flags = [] 32 | self.labels = [] 33 | 34 | self.thread_id = None 35 | self.thread = [] 36 | self.message_id = None 37 | 38 | self.attachments = None 39 | 40 | 41 | 42 | def is_read(self): 43 | return ('\\Seen' in self.flags) 44 | 45 | def read(self): 46 | flag = '\\Seen' 47 | self.gmail.imap.uid('STORE', self.uid, '+FLAGS', flag) 48 | if flag not in self.flags: self.flags.append(flag) 49 | 50 | def unread(self): 51 | flag = '\\Seen' 52 | self.gmail.imap.uid('STORE', self.uid, '-FLAGS', flag) 53 | if flag in self.flags: self.flags.remove(flag) 54 | 55 | def is_starred(self): 56 | return ('\\Flagged' in self.flags) 57 | 58 | def star(self): 59 | flag = '\\Flagged' 60 | self.gmail.imap.uid('STORE', self.uid, '+FLAGS', flag) 61 | if flag not in self.flags: self.flags.append(flag) 62 | 63 | def unstar(self): 64 | flag = '\\Flagged' 65 | self.gmail.imap.uid('STORE', self.uid, '-FLAGS', flag) 66 | if flag in self.flags: self.flags.remove(flag) 67 | 68 | def is_draft(self): 69 | return ('\\Draft' in self.flags) 70 | 71 | def has_label(self, label): 72 | full_label = '%s' % label 73 | return (full_label in self.labels) 74 | 75 | def add_label(self, label): 76 | full_label = '%s' % label 77 | self.gmail.imap.uid('STORE', self.uid, '+X-GM-LABELS', full_label) 78 | if full_label not in self.labels: self.labels.append(full_label) 79 | 80 | def remove_label(self, label): 81 | full_label = '%s' % label 82 | self.gmail.imap.uid('STORE', self.uid, '-X-GM-LABELS', full_label) 83 | if full_label in self.labels: self.labels.remove(full_label) 84 | 85 | 86 | def is_deleted(self): 87 | return ('\\Deleted' in self.flags) 88 | 89 | def delete(self): 90 | flag = '\\Deleted' 91 | self.gmail.imap.uid('STORE', self.uid, '+FLAGS', flag) 92 | if flag not in self.flags: self.flags.append(flag) 93 | 94 | trash = '[Gmail]/Trash' if '[Gmail]/Trash' in self.gmail.labels() else '[Gmail]/Bin' 95 | if self.mailbox.name not in ['[Gmail]/Bin', '[Gmail]/Trash']: 96 | self.move_to(trash) 97 | 98 | # def undelete(self): 99 | # flag = '\\Deleted' 100 | # self.gmail.imap.uid('STORE', self.uid, '-FLAGS', flag) 101 | # if flag in self.flags: self.flags.remove(flag) 102 | 103 | 104 | def move_to(self, name): 105 | self.gmail.copy(self.uid, name, self.mailbox.name) 106 | if name not in ['[Gmail]/Bin', '[Gmail]/Trash']: 107 | self.delete() 108 | 109 | 110 | 111 | def archive(self): 112 | self.move_to('[Gmail]/All Mail') 113 | 114 | def parse_headers(self, message): 115 | hdrs = {} 116 | for hdr in message.keys(): 117 | hdrs[hdr] = message[hdr] 118 | return hdrs 119 | 120 | def parse_flags(self, headers): 121 | return list(ParseFlags(headers)) 122 | # flags = re.search(r'FLAGS \(([^\)]*)\)', headers).groups(1)[0].split(' ') 123 | 124 | def parse_labels(self, headers): 125 | if re.search(r'X-GM-LABELS \(([^\)]+)\)', headers): 126 | labels = re.search(r'X-GM-LABELS \(([^\)]+)\)', headers).groups(1)[0].split(' ') 127 | return map(lambda l: l.replace('"', '').decode("string_escape"), labels) 128 | else: 129 | return list() 130 | 131 | def parse_subject(self, encoded_subject): 132 | dh = decode_header(encoded_subject) 133 | default_charset = 'ASCII' 134 | return ''.join([ unicode(t[0], t[1] or default_charset) for t in dh ]) 135 | 136 | def parse(self, raw_message): 137 | raw_headers = raw_message[0] 138 | raw_email = raw_message[1] 139 | 140 | self.message = email.message_from_string(raw_email) 141 | self.headers = self.parse_headers(self.message) 142 | 143 | self.to = self.message['to'] 144 | self.fr = self.message['from'] 145 | self.delivered_to = self.message['delivered_to'] 146 | 147 | self.subject = self.parse_subject(self.message['subject']) 148 | 149 | if self.message.get_content_maintype() == "multipart": 150 | for content in self.message.walk(): 151 | if content.get_content_type() == "text/plain": 152 | self.body = content.get_payload(decode=True) 153 | elif content.get_content_type() == "text/html": 154 | self.html = content.get_payload(decode=True) 155 | elif self.message.get_content_maintype() == "text": 156 | self.body = self.message.get_payload() 157 | 158 | self.sent_at = datetime.datetime.fromtimestamp(time.mktime(email.utils.parsedate_tz(self.message['date'])[:9])) 159 | 160 | self.flags = self.parse_flags(raw_headers) 161 | 162 | self.labels = self.parse_labels(raw_headers) 163 | 164 | if re.search(r'X-GM-THRID (\d+)', raw_headers): 165 | self.thread_id = re.search(r'X-GM-THRID (\d+)', raw_headers).groups(1)[0] 166 | if re.search(r'X-GM-MSGID (\d+)', raw_headers): 167 | self.message_id = re.search(r'X-GM-MSGID (\d+)', raw_headers).groups(1)[0] 168 | 169 | 170 | # Parse attachments into attachment objects array for this message 171 | self.attachments = [ 172 | Attachment(attachment) for attachment in self.message._payload 173 | if not isinstance(attachment, basestring) and attachment.get('Content-Disposition') is not None 174 | ] 175 | 176 | 177 | def fetch(self): 178 | if not self.message: 179 | response, results = self.gmail.imap.uid('FETCH', self.uid, '(BODY.PEEK[] FLAGS X-GM-THRID X-GM-MSGID X-GM-LABELS)') 180 | 181 | self.parse(results[0]) 182 | 183 | return self.message 184 | 185 | # returns a list of fetched messages (both sent and received) in chronological order 186 | def fetch_thread(self): 187 | self.fetch() 188 | original_mailbox = self.mailbox 189 | self.gmail.use_mailbox(original_mailbox.name) 190 | 191 | # fetch and cache messages from inbox or other received mailbox 192 | response, results = self.gmail.imap.uid('SEARCH', None, '(X-GM-THRID ' + self.thread_id + ')') 193 | received_messages = {} 194 | uids = results[0].split(' ') 195 | if response == 'OK': 196 | for uid in uids: received_messages[uid] = Message(original_mailbox, uid) 197 | self.gmail.fetch_multiple_messages(received_messages) 198 | self.mailbox.messages.update(received_messages) 199 | 200 | # fetch and cache messages from 'sent' 201 | self.gmail.use_mailbox('[Gmail]/Sent Mail') 202 | response, results = self.gmail.imap.uid('SEARCH', None, '(X-GM-THRID ' + self.thread_id + ')') 203 | sent_messages = {} 204 | uids = results[0].split(' ') 205 | if response == 'OK': 206 | for uid in uids: sent_messages[uid] = Message(self.gmail.mailboxes['[Gmail]/Sent Mail'], uid) 207 | self.gmail.fetch_multiple_messages(sent_messages) 208 | self.gmail.mailboxes['[Gmail]/Sent Mail'].messages.update(sent_messages) 209 | 210 | self.gmail.use_mailbox(original_mailbox.name) 211 | 212 | # combine and sort sent and received messages 213 | return sorted(dict(received_messages.items() + sent_messages.items()).values(), key=lambda m: m.sent_at) 214 | 215 | 216 | class Attachment: 217 | 218 | def __init__(self, attachment): 219 | self.name = attachment.get_filename() 220 | # Raw file data 221 | self.payload = attachment.get_payload(decode=True) 222 | # Filesize in kilobytes 223 | self.size = int(round(len(self.payload)/1000.0)) 224 | 225 | def save(self, path=None): 226 | if path is None: 227 | # Save as name of attachment if there is no path specified 228 | path = self.name 229 | elif os.path.isdir(path): 230 | # If the path is a directory, save as name of attachment in that directory 231 | path = os.path.join(path, self.name) 232 | 233 | with open(path, 'wb') as f: 234 | f.write(self.payload) 235 | -------------------------------------------------------------------------------- /gmail/utf.py: -------------------------------------------------------------------------------- 1 | # The contents of this file has been derived code from the Twisted project 2 | # (http://twistedmatrix.com/). The original author is Jp Calderone. 3 | 4 | # Twisted project license follows: 5 | 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files (the 8 | # "Software"), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so, subject to 12 | # the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be 15 | # included in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | text_type = unicode 26 | binary_type = str 27 | 28 | PRINTABLE = set(range(0x20, 0x26)) | set(range(0x27, 0x7f)) 29 | 30 | def encode(s): 31 | """Encode a folder name using IMAP modified UTF-7 encoding. 32 | 33 | Despite the function's name, the output is still a unicode string. 34 | """ 35 | if not isinstance(s, text_type): 36 | return s 37 | 38 | r = [] 39 | _in = [] 40 | 41 | def extend_result_if_chars_buffered(): 42 | if _in: 43 | r.extend(['&', modified_utf7(''.join(_in)), '-']) 44 | del _in[:] 45 | 46 | for c in s: 47 | if ord(c) in PRINTABLE: 48 | extend_result_if_chars_buffered() 49 | r.append(c) 50 | elif c == '&': 51 | extend_result_if_chars_buffered() 52 | r.append('&-') 53 | else: 54 | _in.append(c) 55 | 56 | extend_result_if_chars_buffered() 57 | 58 | return ''.join(r) 59 | 60 | def decode(s): 61 | """Decode a folder name from IMAP modified UTF-7 encoding to unicode. 62 | 63 | Despite the function's name, the input may still be a unicode 64 | string. If the input is bytes, it's first decoded to unicode. 65 | """ 66 | if isinstance(s, binary_type): 67 | s = s.decode('latin-1') 68 | if not isinstance(s, text_type): 69 | return s 70 | 71 | r = [] 72 | _in = [] 73 | for c in s: 74 | if c == '&' and not _in: 75 | _in.append('&') 76 | elif c == '-' and _in: 77 | if len(_in) == 1: 78 | r.append('&') 79 | else: 80 | r.append(modified_deutf7(''.join(_in[1:]))) 81 | _in = [] 82 | elif _in: 83 | _in.append(c) 84 | else: 85 | r.append(c) 86 | if _in: 87 | r.append(modified_deutf7(''.join(_in[1:]))) 88 | 89 | return ''.join(r) 90 | 91 | def modified_utf7(s): 92 | # encode to utf-7: '\xff' => b'+AP8-', decode from latin-1 => '+AP8-' 93 | s_utf7 = s.encode('utf-7').decode('latin-1') 94 | return s_utf7[1:-1].replace('/', ',') 95 | 96 | def modified_deutf7(s): 97 | s_utf7 = '+' + s.replace(',', '/') + '-' 98 | # encode to latin-1: '+AP8-' => b'+AP8-', decode from utf-7 => '\xff' 99 | return s_utf7.encode('latin-1').decode('utf-7') -------------------------------------------------------------------------------- /gmail/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from .gmail import Gmail 4 | 5 | def login(username, password): 6 | gmail = Gmail() 7 | gmail.login(username, password) 8 | return gmail 9 | 10 | def authenticate(username, access_token): 11 | gmail = Gmail() 12 | gmail.authenticate(username, access_token) 13 | return gmail -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | # Utility function to read the README file. 5 | # Used for the long_description. It's nice, because now 1) we have a top level 6 | # README file and 2) it's easier to type in the README file than to put a raw 7 | # string in below ... 8 | def read(fname): 9 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 10 | 11 | setup( 12 | name = "gmail", 13 | version = "0.0.5", 14 | author = "Charlie Guo", 15 | author_email = "FIXME", 16 | description = ("A Pythonic interface for Google Mail."), 17 | license = "MIT", 18 | keywords = "google gmail", 19 | url = "https://github.com/charlierguo/gmail", 20 | packages=['gmail'], 21 | long_description=read('README.md'), 22 | classifiers=[ 23 | "Development Status :: 3 - Alpha", 24 | "Topic :: Communications :: Email", 25 | "License :: OSI Approved :: MIT License", 26 | ], 27 | ) 28 | --------------------------------------------------------------------------------