improved IMAP adapter, thanks spametki
This commit is contained in:
@@ -1 +1 @@
|
|||||||
Version 1.99.4 (2012-01-22 16:08:57) stable
|
Version 1.99.4 (2012-01-23 08:26:04) stable
|
||||||
|
|||||||
+403
-89
@@ -4291,19 +4291,98 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
email IMAP servers to perform simple queries in the
|
email IMAP servers to perform simple queries in the
|
||||||
web2py DAL query syntax, so email read, search and
|
web2py DAL query syntax, so email read, search and
|
||||||
other related IMAP mail services (as those implemented
|
other related IMAP mail services (as those implemented
|
||||||
by brands like Google(r), Hotmail(r) and Yahoo!(r)
|
by brands like Google(r), and Yahoo!(r)
|
||||||
can be managed from web2py applications.
|
can be managed from web2py applications.
|
||||||
|
|
||||||
The code uses examples by Yuji Tomita on this post:
|
The code uses examples by Yuji Tomita on this post:
|
||||||
http://yuji.wordpress.com/2011/06/22/python-imaplib-imap-example-with-gmail/#comment-1137
|
http://yuji.wordpress.com/2011/06/22/python-imaplib-imap-example-with-gmail/#comment-1137
|
||||||
|
and is based in docs for Python imaplib, python email
|
||||||
And IMAP docs for Python imaplib and IETF's RFC2060
|
and email IETF's (i.e. RFC2060 and RFC3501)
|
||||||
|
|
||||||
This adapter was tested with a small set of operations with Gmail(r). Other
|
This adapter was tested with a small set of operations with Gmail(r). Other
|
||||||
services requests could raise command syntax and response data issues.
|
services requests could raise command syntax and response data issues.
|
||||||
|
|
||||||
|
It creates its table and field names "statically",
|
||||||
|
meaning that the developer should leave the table and field
|
||||||
|
definitions to the DAL instance by calling the adapter's
|
||||||
|
.define_tables() method. The tables are defined with the
|
||||||
|
IMAP server mailbox list information.
|
||||||
|
|
||||||
|
Here is a list of supported fields:
|
||||||
|
|
||||||
|
Field Type Description
|
||||||
|
################################################################
|
||||||
|
uid string
|
||||||
|
answered boolean Flag
|
||||||
|
created date
|
||||||
|
content list:string A list of text or html parts
|
||||||
|
to string
|
||||||
|
cc string
|
||||||
|
bcc string
|
||||||
|
size integer the amount of octets of the message*
|
||||||
|
deleted boolean Flag
|
||||||
|
draft boolean Flag
|
||||||
|
flagged boolean Flag
|
||||||
|
sender string
|
||||||
|
recent boolean Flag
|
||||||
|
seen boolean Flag
|
||||||
|
subject string
|
||||||
|
mime string The mime header declaration
|
||||||
|
email string The complete RFC822 message**
|
||||||
|
attachments list:string Each non text decoded part as string
|
||||||
|
|
||||||
|
*At the application side it is measured as the length of the RFC822
|
||||||
|
message string
|
||||||
|
|
||||||
|
WARNING: As row id's are mapped to email sequence numbers,
|
||||||
|
make sure your imap client web2py app does not delete messages
|
||||||
|
during select or update actions, to prevent
|
||||||
|
updating or deleting different messages.
|
||||||
|
Sequence numbers change whenever the mailbox is updated.
|
||||||
|
To avoid this sequence numbers issues, it is recommended the use
|
||||||
|
of uid fields in query references (although the update and delete
|
||||||
|
in separate actions rule still applies).
|
||||||
|
|
||||||
|
# This is the code recommended to start imap support
|
||||||
|
# at the app's model:
|
||||||
|
|
||||||
|
imapdb = DAL("imap://user:password@server:port", pool_size=1) # port 993 for ssl
|
||||||
|
imapdb.define_tables()
|
||||||
|
|
||||||
|
Here is an (incomplete) list of possible imap commands:
|
||||||
|
|
||||||
|
# Count today's unseen messages
|
||||||
|
# smaller than 6000 octets from the
|
||||||
|
# inbox mailbox
|
||||||
|
|
||||||
|
q = imapdb.INBOX.seen == False
|
||||||
|
q &= imapdb.INBOX.created == datetime.date.today()
|
||||||
|
q &= imapdb.INBOX.size < 6000
|
||||||
|
unread = imapdb(q).count()
|
||||||
|
|
||||||
|
# Fetch last query messages
|
||||||
|
rows = imapdb(q).select()
|
||||||
|
|
||||||
|
# it is also possible to filter query select results with limitby and
|
||||||
|
# sequences of mailbox fields
|
||||||
|
|
||||||
|
set.select(<fields sequence>, limitby=(<int>, <int>))
|
||||||
|
|
||||||
|
# Mark last query messages as seen
|
||||||
|
messages = [row.uid for row in rows]
|
||||||
|
seen = imapdb(imapdb.INBOX.uid.belongs(messages)).update(seen=True)
|
||||||
|
|
||||||
|
# Delete messages in the imap database that have mails from mr. Gumby
|
||||||
|
|
||||||
|
deleted = 0
|
||||||
|
for mailbox in imapdb.tables
|
||||||
|
deleted += imapdb(imapdb[mailbox].sender.contains("gumby")).delete()
|
||||||
|
|
||||||
|
# It is possible also to mark messages for deletion instead of ereasing them
|
||||||
|
# directly with set.update(deleted=True)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
types = {
|
types = {
|
||||||
'string': str,
|
'string': str,
|
||||||
'text': str,
|
'text': str,
|
||||||
@@ -4313,9 +4392,11 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
'boolean': bool,
|
'boolean': bool,
|
||||||
'integer': int,
|
'integer': int,
|
||||||
'blob': str,
|
'blob': str,
|
||||||
|
'list:string': str,
|
||||||
}
|
}
|
||||||
|
|
||||||
dbengine = 'imap'
|
dbengine = 'imap'
|
||||||
|
driver = globals().get('imaplib',None)
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
db,
|
db,
|
||||||
@@ -4328,36 +4409,36 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
adapter_args={}):
|
adapter_args={}):
|
||||||
|
|
||||||
# db uri: user@example.com:password@imap.server.com:123
|
# db uri: user@example.com:password@imap.server.com:123
|
||||||
|
# TODO: max size adapter argument for preventing large mail transfers
|
||||||
|
|
||||||
uri = uri.split("://")[1]
|
uri = uri.split("://")[1]
|
||||||
self.db = db
|
self.db = db
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
self.pool_size=0
|
self.pool_size=pool_size
|
||||||
self.folder = folder
|
self.folder = folder
|
||||||
self.db_codec = db_codec
|
self.db_codec = db_codec
|
||||||
self.credential_decoder = credential_decoder
|
self.credential_decoder = credential_decoder
|
||||||
self.driver_args = driver_args
|
self.driver_args = driver_args
|
||||||
self.adapter_args = adapter_args
|
self.adapter_args = adapter_args
|
||||||
self.mailbox_size = None
|
self.mailbox_size = None
|
||||||
self.mailbox_names = dict()
|
self.charset = sys.getfilesystemencoding()
|
||||||
self.encoding = sys.getfilesystemencoding()
|
# imap class
|
||||||
|
self.imap4 = None
|
||||||
|
|
||||||
""" MESSAGE is an identifier for sequence number"""
|
""" MESSAGE is an identifier for sequence number"""
|
||||||
|
|
||||||
|
self.flags = ['\\Deleted', '\\Draft', '\\Flagged',
|
||||||
|
'\\Recent', '\\Seen', '\\Answered']
|
||||||
self.search_fields = {
|
self.search_fields = {
|
||||||
'id': 'MESSAGE',
|
'id': 'MESSAGE', 'created': 'DATE',
|
||||||
'created': 'DATE',
|
'uid': 'UID', 'sender': 'FROM',
|
||||||
'uid': 'UID',
|
'to': 'TO', 'cc': 'CC',
|
||||||
'sender': 'FROM',
|
'bcc': 'BCC', 'content': 'TEXT',
|
||||||
'to': 'TO',
|
'size': 'SIZE', 'deleted': '\\Deleted',
|
||||||
'content': 'TEXT',
|
'draft': '\\Draft', 'flagged': '\\Flagged',
|
||||||
'deleted': '\\Deleted',
|
'recent': '\\Recent', 'seen': '\\Seen',
|
||||||
'draft': '\\Draft',
|
'subject': 'SUBJECT', 'answered': '\\Answered',
|
||||||
'flagged': '\\Flagged',
|
'mime': None, 'email': None,
|
||||||
'recent': '\\Recent',
|
|
||||||
'seen': '\\Seen',
|
|
||||||
'subject': 'SUBJECT',
|
|
||||||
'answered': '\\Answered',
|
|
||||||
'mime': None,
|
|
||||||
'email': None,
|
|
||||||
'attachments': None
|
'attachments': None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4377,31 +4458,88 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
# it is assumed sucessful authentication alLways
|
# it is assumed sucessful authentication alLways
|
||||||
# TODO: support direct connection and login tests
|
# TODO: support direct connection and login tests
|
||||||
if over_ssl:
|
if over_ssl:
|
||||||
imap4 = imaplib.IMAP4_SSL
|
self.imap4 = self.driver.IMAP4_SSL
|
||||||
else:
|
else:
|
||||||
imap4 = imaplib.IMAP4
|
self.imap4 = self.driver.IMAP4
|
||||||
connection = imap4(driver_args["host"], driver_args["port"])
|
connection = self.imap4(driver_args["host"], driver_args["port"])
|
||||||
connection.login(driver_args["user"], driver_args["password"])
|
data = connection.login(driver_args["user"], driver_args["password"])
|
||||||
|
# print "Connected to remote server"
|
||||||
|
# print data
|
||||||
|
# static mailbox list
|
||||||
|
connection.mailbox_names = None
|
||||||
|
|
||||||
|
# dummy cursor function
|
||||||
|
connection.cursor = lambda : True
|
||||||
|
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
self.pool_connection(connect,cursor=False)
|
self.pool_connection(connect)
|
||||||
self.db.define_tables = self.define_tables
|
self.db.define_tables = self.define_tables
|
||||||
|
|
||||||
|
def pool_connection(self, f, cursor=True):
|
||||||
|
"""
|
||||||
|
IMAP4 Pool connection method
|
||||||
|
|
||||||
|
imap connection lacks of self cursor command.
|
||||||
|
A custom command should be provided as a replacement
|
||||||
|
for connection pooling to prevent uncaught remote session
|
||||||
|
closing
|
||||||
|
|
||||||
|
"""
|
||||||
|
# print "Pool Connection"
|
||||||
|
if not self.pool_size:
|
||||||
|
self.connection = f()
|
||||||
|
self.cursor = cursor and self.connection.cursor()
|
||||||
|
else:
|
||||||
|
uri = self.uri
|
||||||
|
# print "uri", self.uri
|
||||||
|
while True:
|
||||||
|
sql_locker.acquire()
|
||||||
|
if not uri in ConnectionPool.pools:
|
||||||
|
ConnectionPool.pools[uri] = []
|
||||||
|
if ConnectionPool.pools[uri]:
|
||||||
|
self.connection = ConnectionPool.pools[uri].pop()
|
||||||
|
sql_locker.release()
|
||||||
|
self.cursor = cursor and self.connection.cursor()
|
||||||
|
# print "self.cursor", self.cursor
|
||||||
|
if self.cursor and self.check_active_connection:
|
||||||
|
try:
|
||||||
|
# check if connection is alive or close it
|
||||||
|
result, data = self.connection.list()
|
||||||
|
# print "Checked connection"
|
||||||
|
# print result, data
|
||||||
|
# self.execute('SELECT 1;')
|
||||||
|
except:
|
||||||
|
# Possible connection reset error
|
||||||
|
# TODO: read exception class
|
||||||
|
# print "Re-connecting to IMAP server"
|
||||||
|
self.connection = f()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
sql_locker.release()
|
||||||
|
self.connection = f()
|
||||||
|
self.cursor = cursor and self.connection.cursor()
|
||||||
|
break
|
||||||
|
|
||||||
|
if not hasattr(thread,'instances'):
|
||||||
|
thread.instances = []
|
||||||
|
thread.instances.append(self)
|
||||||
|
|
||||||
def get_last_message(self, tablename):
|
def get_last_message(self, tablename):
|
||||||
last_message = None
|
last_message = None
|
||||||
# request mailbox list to the server
|
# request mailbox list to the server
|
||||||
# if needed
|
# if needed
|
||||||
if not len(self.mailbox_names.keys()) > 0:
|
if not isinstance(self.connection.mailbox_names, dict):
|
||||||
self.get_mailboxes()
|
self.get_mailboxes()
|
||||||
try:
|
try:
|
||||||
result = self.connection.select(self.mailbox_names[tablename])
|
result = self.connection.select(self.connection.mailbox_names[tablename])
|
||||||
last_message = int(result[1][0])
|
last_message = int(result[1][0])
|
||||||
except (IndexError, ValueError, TypeError, KeyError), e:
|
except (IndexError, ValueError, TypeError, KeyError), e:
|
||||||
logger.debug("Error retrieving the last mailbox sequence number. %s" % str(e))
|
logger.debug("Error retrieving the last mailbox sequence number. %s" % str(e))
|
||||||
return last_message
|
return last_message
|
||||||
|
|
||||||
def get_uid_bounds(self, tablename):
|
def get_uid_bounds(self, tablename):
|
||||||
if not len(self.mailbox_names.keys()) > 0:
|
if not isinstance(self.connection.mailbox_names, dict):
|
||||||
self.get_mailboxes()
|
self.get_mailboxes()
|
||||||
# fetch first and last messages
|
# fetch first and last messages
|
||||||
# return (first, last) messages uid's
|
# return (first, last) messages uid's
|
||||||
@@ -4444,17 +4582,32 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def decode_text(self):
|
def encode_text(self, text, charset, errors="replace"):
|
||||||
""" translate encoded text for mail to unicode"""
|
""" convert text for mail to unicode"""
|
||||||
# not implemented
|
if text is None:
|
||||||
pass
|
text = ""
|
||||||
|
else:
|
||||||
|
if isinstance(text, str):
|
||||||
|
if charset is not None:
|
||||||
|
text = unicode(text, charset, errors)
|
||||||
|
else:
|
||||||
|
text = unicode(text, "utf-8", errors)
|
||||||
|
else:
|
||||||
|
raise Exception("Unsupported mail text type %s" % type(text))
|
||||||
|
return text.encode("utf-8")
|
||||||
|
|
||||||
def get_charset(self, message):
|
def get_charset(self, message):
|
||||||
charset = message.get_content_charset()
|
charset = message.get_content_charset()
|
||||||
return charset
|
return charset
|
||||||
|
|
||||||
|
def reset_mailboxes(self):
|
||||||
|
self.connection.mailbox_names = None
|
||||||
|
self.get_mailboxes()
|
||||||
|
|
||||||
def get_mailboxes(self):
|
def get_mailboxes(self):
|
||||||
|
""" Query the mail database for mailbox names """
|
||||||
mailboxes_list = self.connection.list()
|
mailboxes_list = self.connection.list()
|
||||||
|
self.connection.mailbox_names = dict()
|
||||||
mailboxes = list()
|
mailboxes = list()
|
||||||
for item in mailboxes_list[1]:
|
for item in mailboxes_list[1]:
|
||||||
item = item.strip()
|
item = item.strip()
|
||||||
@@ -4465,9 +4618,33 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
# remove unwanted characters and store original names
|
# remove unwanted characters and store original names
|
||||||
mailbox_name = mailbox.replace("[", "").replace("]", "").replace("/", "_")
|
mailbox_name = mailbox.replace("[", "").replace("]", "").replace("/", "_")
|
||||||
mailboxes.append(mailbox_name)
|
mailboxes.append(mailbox_name)
|
||||||
self.mailbox_names[mailbox_name] = mailbox
|
self.connection.mailbox_names[mailbox_name] = mailbox
|
||||||
|
# print "Mailboxes query", mailboxes
|
||||||
return mailboxes
|
return mailboxes
|
||||||
|
|
||||||
|
def get_query_mailbox(self, query):
|
||||||
|
nofield = True
|
||||||
|
tablename = None
|
||||||
|
attr = query
|
||||||
|
while nofield:
|
||||||
|
if hasattr(attr, "first"):
|
||||||
|
attr = attr.first
|
||||||
|
if isinstance(attr, Field):
|
||||||
|
return attr.tablename
|
||||||
|
elif isinstance(attr, Query):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return tablename
|
||||||
|
|
||||||
|
def is_flag(self, flag):
|
||||||
|
if self.search_fields.get(flag, None) in self.flags:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def define_tables(self):
|
def define_tables(self):
|
||||||
"""
|
"""
|
||||||
Auto create common IMAP fileds
|
Auto create common IMAP fileds
|
||||||
@@ -4477,24 +4654,29 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
not be supported and definitions handled on a service/mode
|
not be supported and definitions handled on a service/mode
|
||||||
basis (local syntax for Gmail(r), Ymail(r)
|
basis (local syntax for Gmail(r), Ymail(r)
|
||||||
"""
|
"""
|
||||||
mailboxes = self.get_mailboxes()
|
if not isinstance(self.connection.mailbox_names, dict):
|
||||||
|
self.get_mailboxes()
|
||||||
|
mailboxes = self.connection.mailbox_names.keys()
|
||||||
for mailbox_name in mailboxes:
|
for mailbox_name in mailboxes:
|
||||||
self.db.define_table("%s" % mailbox_name,
|
self.db.define_table("%s" % mailbox_name,
|
||||||
Field("uid", "string", writable=False),
|
Field("uid", "string", writable=False),
|
||||||
Field("answered", "boolean", writable=False),
|
Field("answered", "boolean"),
|
||||||
Field("created", "datetime", writable=False),
|
Field("created", "datetime", writable=False),
|
||||||
Field("content", "list:text", writable=False),
|
Field("content", "list:string", writable=False),
|
||||||
Field("to", "string", writable=False),
|
Field("to", "string", writable=False),
|
||||||
Field("deleted", "boolean", writable=False),
|
Field("cc", "string", writable=False),
|
||||||
Field("draft", "boolean", writable=False),
|
Field("bcc", "string", writable=False),
|
||||||
Field("flagged", "boolean", writable=False),
|
Field("size", "integer", writable=False),
|
||||||
|
Field("deleted", "boolean"),
|
||||||
|
Field("draft", "boolean"),
|
||||||
|
Field("flagged", "boolean"),
|
||||||
Field("sender", "string", writable=False),
|
Field("sender", "string", writable=False),
|
||||||
Field("recent", "boolean", writable=False),
|
Field("recent", "boolean", writable=False),
|
||||||
Field("seen", "boolean", writable=False),
|
Field("seen", "boolean"),
|
||||||
Field("subject", "string", writable=False),
|
Field("subject", "string", writable=False),
|
||||||
Field("mime", "string", writable=False),
|
Field("mime", "string", writable=False),
|
||||||
Field("email", "text", writable=False),
|
Field("email", "string", writable=False, readable=False),
|
||||||
Field("attachments", "list:text", writable=False),
|
Field("attachments", "list:string", writable=False, readable=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_table(self, *args, **kwargs):
|
def create_table(self, *args, **kwargs):
|
||||||
@@ -4506,6 +4688,9 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
rows
|
rows
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not query.ignore_common_filters:
|
||||||
|
query = self.common_filter(query, [self.get_query_mailbox(query),])
|
||||||
|
|
||||||
# move this statement elsewhere (upper-level)
|
# move this statement elsewhere (upper-level)
|
||||||
import email
|
import email
|
||||||
import email.header
|
import email.header
|
||||||
@@ -4513,9 +4698,10 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
# get records from imap server with search + fetch
|
# get records from imap server with search + fetch
|
||||||
# convert results to a dictionary
|
# convert results to a dictionary
|
||||||
tablename = None
|
tablename = None
|
||||||
|
fetch_results = list()
|
||||||
if isinstance(query, (Expression, Query)):
|
if isinstance(query, (Expression, Query)):
|
||||||
tablename = self.get_table(query)
|
tablename = self.get_table(query)
|
||||||
mailbox = self.mailbox_names.get(tablename, None)
|
mailbox = self.connection.mailbox_names.get(tablename, None)
|
||||||
if isinstance(query, Expression):
|
if isinstance(query, Expression):
|
||||||
pass
|
pass
|
||||||
elif isinstance(query, Query):
|
elif isinstance(query, Query):
|
||||||
@@ -4524,15 +4710,25 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
selected = self.connection.select(mailbox, True)
|
selected = self.connection.select(mailbox, True)
|
||||||
self.mailbox_size = int(selected[1][0])
|
self.mailbox_size = int(selected[1][0])
|
||||||
search_query = "(%s)" % str(query).strip()
|
search_query = "(%s)" % str(query).strip()
|
||||||
|
# print "Query", query
|
||||||
|
# print "Search query", search_query
|
||||||
search_result = self.connection.uid("search", None, search_query)
|
search_result = self.connection.uid("search", None, search_query)
|
||||||
|
# print "Search result", search_result
|
||||||
|
# print search_result
|
||||||
# Normal IMAP response OK is assumed (change this)
|
# Normal IMAP response OK is assumed (change this)
|
||||||
if search_result[0] == "OK":
|
if search_result[0] == "OK":
|
||||||
fetch_results = list()
|
|
||||||
# For "light" remote server responses just get the first
|
# For "light" remote server responses just get the first
|
||||||
# ten records (change for non-experimental implementation)
|
# ten records (change for non-experimental implementation)
|
||||||
# However, light responses are not guaranteed with this
|
# However, light responses are not guaranteed with this
|
||||||
# approach, just fewer messages.
|
# approach, just fewer messages.
|
||||||
messages_set = search_result[1][0].split()[:10]
|
# TODO: change limitby single to 2-tuple argument
|
||||||
|
limitby = attributes.get('limitby', None)
|
||||||
|
messages_set = search_result[1][0].split()
|
||||||
|
# descending order
|
||||||
|
messages_set.reverse()
|
||||||
|
if limitby is not None:
|
||||||
|
# TODO: asc/desc attributes
|
||||||
|
messages_set = messages_set[int(limitby[0]):int(limitby[1])]
|
||||||
# Partial fetches are not used since the email
|
# Partial fetches are not used since the email
|
||||||
# library does not seem to support it (it converts
|
# library does not seem to support it (it converts
|
||||||
# partial messages to mangled message instances)
|
# partial messages to mangled message instances)
|
||||||
@@ -4543,15 +4739,31 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
# (change to multi-fetch command syntax for faster
|
# (change to multi-fetch command syntax for faster
|
||||||
# transactions)
|
# transactions)
|
||||||
for uid in messages_set:
|
for uid in messages_set:
|
||||||
|
# fetch the RFC822 message body
|
||||||
typ, data = self.connection.uid("fetch", uid, imap_fields)
|
typ, data = self.connection.uid("fetch", uid, imap_fields)
|
||||||
fr = {"message": int(data[0][0].split()[0]),
|
if typ == "OK":
|
||||||
"uid": int(uid),
|
fr = {"message": int(data[0][0].split()[0]),
|
||||||
"email": email.message_from_string(data[0][1])
|
"uid": int(uid),
|
||||||
}
|
"email": email.message_from_string(data[0][1]),
|
||||||
fr["multipart"] = fr["email"].is_multipart()
|
"raw_message": data[0][1]
|
||||||
fetch_results.append(fr)
|
}
|
||||||
|
fr["multipart"] = fr["email"].is_multipart()
|
||||||
|
# fetch flags for the message
|
||||||
|
ftyp, fdata = self.connection.uid("fetch", uid, "(FLAGS)")
|
||||||
|
if ftyp == "OK":
|
||||||
|
# print "Raw flags", fdata
|
||||||
|
fr["flags"] = self.driver.ParseFlags(fdata[0])
|
||||||
|
# print "Flags", fr["flags"]
|
||||||
|
fetch_results.append(fr)
|
||||||
|
else:
|
||||||
|
# error retrieving the flags for this message
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# error retrieving the message body
|
||||||
|
pass
|
||||||
|
|
||||||
elif isinstance(query, basestring):
|
elif isinstance(query, basestring):
|
||||||
|
# not implemented
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
@@ -4576,11 +4788,16 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
imapqry_list = list()
|
imapqry_list = list()
|
||||||
imapqry_array = list()
|
imapqry_array = list()
|
||||||
for fr in fetch_results:
|
for fr in fetch_results:
|
||||||
|
attachments = []
|
||||||
|
content = []
|
||||||
|
size = 0
|
||||||
n = int(fr["message"])
|
n = int(fr["message"])
|
||||||
item_dict = dict()
|
item_dict = dict()
|
||||||
message = fr["email"]
|
message = fr["email"]
|
||||||
uid = fr["uid"]
|
uid = fr["uid"]
|
||||||
charset = self.get_charset(message)
|
charset = self.get_charset(message)
|
||||||
|
flags = fr["flags"]
|
||||||
|
raw_message = fr["raw_message"]
|
||||||
# Return messages data mapping static fields
|
# Return messages data mapping static fields
|
||||||
# and fetched results. Mapping should be made
|
# and fetched results. Mapping should be made
|
||||||
# outside the select function (with auxiliary
|
# outside the select function (with auxiliary
|
||||||
@@ -4598,57 +4815,75 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
if "%s.sender" % tablename in fieldnames:
|
if "%s.sender" % tablename in fieldnames:
|
||||||
# If there is no encoding found in the message header
|
# If there is no encoding found in the message header
|
||||||
# force utf-8 replacing characters (change this to
|
# force utf-8 replacing characters (change this to
|
||||||
# module's defaults). Applies to .sender and .to fields
|
# module's defaults). Applies to .sender, .to, .cc and .bcc fields
|
||||||
if charset is not None:
|
#############################################################################
|
||||||
item_dict["%s.sender" % tablename] = unicode(message["From"], charset, "replace")
|
# TODO: External function to manage encoding and decoding of message strings
|
||||||
else:
|
#############################################################################
|
||||||
item_dict["%s.sender" % tablename] = unicode(message["From"], "utf-8", "replace")
|
item_dict["%s.sender" % tablename] = self.encode_text(message["From"], charset)
|
||||||
if "%s.to" % tablename in fieldnames:
|
if "%s.to" % tablename in fieldnames:
|
||||||
if charset is not None:
|
item_dict["%s.to" % tablename] = self.encode_text(message["To"], charset)
|
||||||
item_dict["%s.to" % tablename] = unicode(message["To"], charset, "replace")
|
if "%s.cc" % tablename in fieldnames:
|
||||||
|
if "Cc" in message.keys():
|
||||||
|
# print "cc field found"
|
||||||
|
item_dict["%s.cc" % tablename] = self.encode_text(message["Cc"], charset)
|
||||||
else:
|
else:
|
||||||
item_dict["%s.to" % tablename] = unicode(message["To"], "utf-8", "replace")
|
item_dict["%s.cc" % tablename] = ""
|
||||||
if "%s.content" % tablename in fieldnames:
|
if "%s.bcc" % tablename in fieldnames:
|
||||||
content = []
|
if "Bcc" in message.keys():
|
||||||
for part in message.walk():
|
# print "bcc field found"
|
||||||
if "text" in part.get_content_maintype():
|
item_dict["%s.bcc" % tablename] = self.encode_text(message["Bcc"], charset)
|
||||||
payload = part.get_payload(decode=True)
|
else:
|
||||||
content.append(payload)
|
item_dict["%s.bcc" % tablename] = ""
|
||||||
item_dict["%s.content" % tablename] = content
|
|
||||||
if "%s.deleted" % tablename in fieldnames:
|
if "%s.deleted" % tablename in fieldnames:
|
||||||
item_dict["%s.deleted" % tablename] = None
|
item_dict["%s.deleted" % tablename] = "\\Deleted" in flags
|
||||||
if "%s.draft" % tablename in fieldnames:
|
if "%s.draft" % tablename in fieldnames:
|
||||||
item_dict["%s.draft" % tablename] = None
|
item_dict["%s.draft" % tablename] = "\\Draft" in flags
|
||||||
if "%s.flagged" % tablename in fieldnames:
|
if "%s.flagged" % tablename in fieldnames:
|
||||||
item_dict["%s.flagged" % tablename] = None
|
item_dict["%s.flagged" % tablename] = "\\Flagged" in flags
|
||||||
if "%s.recent" % tablename in fieldnames:
|
if "%s.recent" % tablename in fieldnames:
|
||||||
item_dict["%s.recent" % tablename] = None
|
item_dict["%s.recent" % tablename] = "\\Recent" in flags
|
||||||
if "%s.seen" % tablename in fieldnames:
|
if "%s.seen" % tablename in fieldnames:
|
||||||
item_dict["%s.seen" % tablename] = None
|
item_dict["%s.seen" % tablename] = "\\Seen" in flags
|
||||||
if "%s.subject" % tablename in fieldnames:
|
if "%s.subject" % tablename in fieldnames:
|
||||||
subject = message["Subject"]
|
subject = message["Subject"]
|
||||||
decoded_subject = decode_header(subject)
|
decoded_subject = decode_header(subject)
|
||||||
text = decoded_subject[0][0]
|
text = decoded_subject[0][0]
|
||||||
encoding = decoded_subject[0][1]
|
encoding = decoded_subject[0][1]
|
||||||
if encoding is not None:
|
if encoding in (None, ""):
|
||||||
text = unicode(text, encoding)
|
encoding = charset
|
||||||
item_dict["%s.subject" % tablename] = text
|
item_dict["%s.subject" % tablename] = self.encode_text(text, encoding)
|
||||||
if "%s.answered" % tablename in fieldnames:
|
if "%s.answered" % tablename in fieldnames:
|
||||||
item_dict["%s.answered" % tablename] = None
|
item_dict["%s.answered" % tablename] = "\\Answered" in flags
|
||||||
if "%s.mime" % tablename in fieldnames:
|
if "%s.mime" % tablename in fieldnames:
|
||||||
item_dict["%s.mime" % tablename] = message.get_content_type()
|
item_dict["%s.mime" % tablename] = message.get_content_type()
|
||||||
|
# Here goes the whole RFC822 body as an email instance
|
||||||
# here goes the whole RFC822 body as an email instance
|
|
||||||
# for controller side custom processing
|
# for controller side custom processing
|
||||||
|
# The message is stored as a raw string
|
||||||
|
# >> email.message_from_string(raw string)
|
||||||
|
# returns a Message object for enhanced object processing
|
||||||
if "%s.email" % tablename in fieldnames:
|
if "%s.email" % tablename in fieldnames:
|
||||||
item_dict["%s.email" % tablename] = message
|
item_dict["%s.email" % tablename] = self.encode_text(raw_message, charset)
|
||||||
|
# Size measure as suggested in a Velocity Reviews post
|
||||||
if "%s.attachments" % tablename in fieldnames:
|
# by Tim Williams: "how to get size of email attachment"
|
||||||
attachments = []
|
# Note: len() and server RFC822.SIZE reports doesn't match
|
||||||
for part in message.walk():
|
# To retrieve the server size for representation would add a new
|
||||||
|
# fetch transaction to the process
|
||||||
|
for part in message.walk():
|
||||||
|
if "%s.attachments" % tablename in fieldnames:
|
||||||
if not "text" in part.get_content_maintype():
|
if not "text" in part.get_content_maintype():
|
||||||
attachments.append(part.get_payload(decode=True))
|
attachments.append(part.get_payload(decode=True))
|
||||||
item_dict["%s.attachments" % tablename] = attachments
|
if "%s.content" % tablename in fieldnames:
|
||||||
|
if "text" in part.get_content_maintype():
|
||||||
|
payload = self.encode_text(part.get_payload(decode=True), charset)
|
||||||
|
content.append(payload)
|
||||||
|
if "%s.size" % tablename in fieldnames:
|
||||||
|
if part is not None:
|
||||||
|
size += len(str(part))
|
||||||
|
|
||||||
|
item_dict["%s.content" % tablename] = bar_encode(content)
|
||||||
|
item_dict["%s.attachments" % tablename] = bar_encode(attachments)
|
||||||
|
item_dict["%s.size" % tablename] = size
|
||||||
|
|
||||||
imapqry_list.append(item_dict)
|
imapqry_list.append(item_dict)
|
||||||
|
|
||||||
# extra object mapping for the sake of rows object
|
# extra object mapping for the sake of rows object
|
||||||
@@ -4665,13 +4900,88 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
tablename, imapqry_array , fieldnames = self._select(query,fields,attributes)
|
tablename, imapqry_array , fieldnames = self._select(query,fields,attributes)
|
||||||
# parse result and return a rows object
|
# parse result and return a rows object
|
||||||
colnames = fieldnames
|
colnames = fieldnames
|
||||||
result = self.parse(imapqry_array, colnames)
|
result = self.parse(imapqry_array, fields, colnames)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def update(self, tablename, query, fields):
|
||||||
|
# print "_update"
|
||||||
|
|
||||||
|
if not query.ignore_common_filters:
|
||||||
|
query = self.common_filter(query, [tablename,])
|
||||||
|
|
||||||
|
mark = []
|
||||||
|
unmark = []
|
||||||
|
rowcount = 0
|
||||||
|
query = str(query)
|
||||||
|
if query:
|
||||||
|
for item in fields:
|
||||||
|
field = item[0]
|
||||||
|
name = field.name
|
||||||
|
value = item[1]
|
||||||
|
if self.is_flag(name):
|
||||||
|
flag = self.search_fields[name]
|
||||||
|
if (value is not None) and (flag != "\\Recent"):
|
||||||
|
if value:
|
||||||
|
mark.append(flag)
|
||||||
|
else:
|
||||||
|
unmark.append(flag)
|
||||||
|
|
||||||
|
# print "Selecting mailbox ..."
|
||||||
|
result, data = self.connection.select(self.connection.mailbox_names[tablename])
|
||||||
|
# print "Retrieving sequence numbers remotely"
|
||||||
|
string_query = "(%s)" % query
|
||||||
|
# print "string query", string_query
|
||||||
|
result, data = self.connection.search(None, string_query)
|
||||||
|
store_list = [item.strip() for item in data[0].split() if item.strip().isdigit()]
|
||||||
|
# print "Storing values..."
|
||||||
|
# change marked flags
|
||||||
|
for number in store_list:
|
||||||
|
result = None
|
||||||
|
if len(mark) > 0:
|
||||||
|
# print "Marking flags ..."
|
||||||
|
result, data = self.connection.store(number, "+FLAGS", "(%s)" % " ".join(mark))
|
||||||
|
if len(unmark) > 0:
|
||||||
|
# print "Unmarking flags ..."
|
||||||
|
result, data = self.connection.store(number, "-FLAGS", "(%s)" % " ".join(unmark))
|
||||||
|
if result == "OK":
|
||||||
|
rowcount += 1
|
||||||
|
return rowcount
|
||||||
|
|
||||||
def count(self,query,distinct=None):
|
def count(self,query,distinct=None):
|
||||||
# not implemented
|
counter = 0
|
||||||
# (count search results without select call)
|
tablename = self.get_query_mailbox(query)
|
||||||
pass
|
if query and tablename is not None:
|
||||||
|
if not query.ignore_common_filters:
|
||||||
|
query = self.common_filter(query, [tablename,])
|
||||||
|
# print "Selecting mailbox ..."
|
||||||
|
result, data = self.connection.select(self.connection.mailbox_names[tablename])
|
||||||
|
# print "Retrieving sequence numbers remotely"
|
||||||
|
string_query = "(%s)" % query
|
||||||
|
result, data = self.connection.search(None, string_query)
|
||||||
|
store_list = [item.strip() for item in data[0].split() if item.strip().isdigit()]
|
||||||
|
counter = len(store_list)
|
||||||
|
return counter
|
||||||
|
|
||||||
|
def delete(self, tablename, query):
|
||||||
|
counter = 0
|
||||||
|
if query:
|
||||||
|
# print "Selecting mailbox ..."
|
||||||
|
if not query.ignore_common_filters:
|
||||||
|
query = self.common_filter(query, [tablename,])
|
||||||
|
result, data = self.connection.select(self.connection.mailbox_names[tablename])
|
||||||
|
# print "Retrieving sequence numbers remotely"
|
||||||
|
string_query = "(%s)" % query
|
||||||
|
result, data = self.connection.search(None, string_query)
|
||||||
|
store_list = [item.strip() for item in data[0].split() if item.strip().isdigit()]
|
||||||
|
for number in store_list:
|
||||||
|
result, data = self.connection.store(number, "+FLAGS", "(\\Deleted)")
|
||||||
|
# print "Deleting message", result, data
|
||||||
|
if result == "OK":
|
||||||
|
counter += 1
|
||||||
|
if counter > 0:
|
||||||
|
# print "Ereasing permanently"
|
||||||
|
result, data = self.connection.expunge()
|
||||||
|
return counter
|
||||||
|
|
||||||
def BELONGS(self, first, second):
|
def BELONGS(self, first, second):
|
||||||
result = None
|
result = None
|
||||||
@@ -4726,6 +5036,8 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
result = "UID %s:%s" % (lower_limit, threshold)
|
result = "UID %s:%s" % (lower_limit, threshold)
|
||||||
elif name == "DATE":
|
elif name == "DATE":
|
||||||
result = "SINCE %s" % self.convert_date(second, add=datetime.timedelta(1))
|
result = "SINCE %s" % self.convert_date(second, add=datetime.timedelta(1))
|
||||||
|
elif name == "SIZE":
|
||||||
|
result = "LARGER %s" % self.expand(second)
|
||||||
else:
|
else:
|
||||||
raise Exception("Operation not supported")
|
raise Exception("Operation not supported")
|
||||||
return result
|
return result
|
||||||
@@ -4771,6 +5083,8 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
result = "UID %s:%s" % (pedestal, upper_limit)
|
result = "UID %s:%s" % (pedestal, upper_limit)
|
||||||
elif name == "DATE":
|
elif name == "DATE":
|
||||||
result = "BEFORE %s" % self.convert_date(second)
|
result = "BEFORE %s" % self.convert_date(second)
|
||||||
|
elif name == "SIZE":
|
||||||
|
result = "SMALLER %s" % self.expand(second)
|
||||||
else:
|
else:
|
||||||
raise Exception("Operation not supported")
|
raise Exception("Operation not supported")
|
||||||
return result
|
return result
|
||||||
@@ -4811,7 +5125,7 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
elif name == "DATE":
|
elif name == "DATE":
|
||||||
result = "ON %s" % self.convert_date(second)
|
result = "ON %s" % self.convert_date(second)
|
||||||
|
|
||||||
elif name in ('\\Deleted', '\\Draft', '\\Flagged', '\\Recent', '\\Seen', '\\Answered'):
|
elif name in self.flags:
|
||||||
if second:
|
if second:
|
||||||
result = "%s" % (name.upper()[1:])
|
result = "%s" % (name.upper()[1:])
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user