Skip to content

Commit

Permalink
Merge pull request #67 from modoboa/feature/use_contacts_plugin
Browse files Browse the repository at this point in the history
Use contacts plugin if installed.
  • Loading branch information
tonioo committed Apr 20, 2017
2 parents bf7092d + d623419 commit 44618c1
Show file tree
Hide file tree
Showing 12 changed files with 336 additions and 207 deletions.
8 changes: 4 additions & 4 deletions modoboa_webmail/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ class ComposeMailForm(forms.Form):
cc = forms.CharField(
label=_("Cc"), required=False, validators=[validate_email_list],
widget=forms.TextInput(
attrs={"placeholder":
_("Enter one or more addresses separated by a comma.")})
attrs={"placeholder": _("Enter one or more addresses."),
"class": "selectize"})
)
bcc = forms.CharField(
label=_("Bcc"), required=False, validators=[validate_email_list],
widget=forms.TextInput(
attrs={"placeholder":
_("Enter one or more addresses separated by a comma.")})
attrs={"placeholder": _("Enter one or more addresses."),
"class": "selectize"})
)

subject = forms.CharField(
Expand Down
126 changes: 67 additions & 59 deletions modoboa_webmail/lib/fetch_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,59 @@ def parse_bodystructure(buf, depth=0, prefix=""):
"End of buffer reached while looking for a BODY/BODYSTRUCTURE end")


def parse_response_chunk(chunk):
"""Parse a piece of response..."""
buf = "".join(chunk)
parts = buf.split(" ", 3)
result = {}
response = parts[3]
while len(response):
if response.startswith('BODY') and response[4] == '[':
end = response.find(']', 5)
if response[end + 1] == '<':
end = response.find('>', end + 1)
end += 1
else:
end = response.find(' ')
cmdname = response[:end]
response = response[end + 1:]

end = 0
if cmdname in ['BODY', 'BODYSTRUCTURE', 'FLAGS']:
parendepth = 0
instring = False
for pos, c in enumerate(response):
if not instring and c == '"':
instring = True
continue
if instring and c == '"':
if pos and response[pos - 1] != '\\':
instring = False
continue
if not instring and c == '(':
parendepth += 1
continue
if not instring and c == ')':
parendepth -= 1
if parendepth == 0:
end = pos + 1
break
else:
token, end = parse_next_token(response)
if isinstance(token, Literal):
response = response[end:]
end = token.next_token_len

result[cmdname] = response[:end]
response = response[end + 1:]
try:
func = globals()["parse_%s" % cmdname.lower()]
result[cmdname] = func(result[cmdname])[0]
except KeyError:
pass
return int(parts[2]), result


def parse_fetch_response(data):
"""Parse a FETCH response, previously issued by a UID command
Expand All @@ -130,67 +183,22 @@ def parse_fetch_response(data):
"""
result = {}
cpt = 0
while cpt < len(data):
content = ()
while cpt < len(data) and data[cpt] != ')':
if isinstance(data[cpt], str):
# FIXME : probably an unsolicited response
if len(data) == 1:
key, value = parse_response_chunk(data[0])
result[key] = value
else:
while cpt < len(data):
content = ()
while cpt < len(data) and data[cpt] != ")":
if isinstance(data[cpt], str):
# FIXME : probably an unsolicited response
cpt += 1
continue
content += data[cpt]
cpt += 1
continue
content += data[cpt]
cpt += 1
cpt += 1

buf = "".join(content)
parts = buf.split(' ', 3)
msgdef = result[int(parts[2])] = {}
response = parts[3]

while len(response):
if response.startswith('BODY') and response[4] == '[':
end = response.find(']', 5)
if response[end + 1] == '<':
end = response.find('>', end + 1)
end += 1
else:
end = response.find(' ')
cmdname = response[:end]
response = response[end + 1:]

end = 0
if cmdname in ['BODY', 'BODYSTRUCTURE', 'FLAGS']:
parendepth = 0
instring = False
for pos, c in enumerate(response):
if not instring and c == '"':
instring = True
continue
if instring and c == '"':
if pos and response[pos - 1] != '\\':
instring = False
continue
if not instring and c == '(':
parendepth += 1
continue
if not instring and c == ')':
parendepth -= 1
if parendepth == 0:
end = pos + 1
break
else:
token, end = parse_next_token(response)
if isinstance(token, Literal):
response = response[end:]
end = token.next_token_len

msgdef[cmdname] = response[:end]
response = response[end + 1:]
try:
func = globals()["parse_%s" % cmdname.lower()]
msgdef[cmdname] = func(msgdef[cmdname])[0]
except KeyError:
pass

key, value = parse_response_chunk(content)
result[key] = value
return result


Expand Down
85 changes: 49 additions & 36 deletions modoboa_webmail/lib/imapemail.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _

from modoboa.core.extensions import exts_pool
from modoboa.lib import u2u_decode
from modoboa.lib.email_utils import Email, EmailAddress

from . import imapheader
from .imaputils import (
get_imapconnector, BodyStructure
)
Expand Down Expand Up @@ -43,14 +46,40 @@ def __init__(self, request, addrfull, *args, **kwargs):
self.imapc = get_imapconnector(request)
self.mbox, self.mailid = self.mailid.split(":")

headers = self.msg['BODY[HEADER.FIELDS (%s)]' % self.headers_as_text]
def _insert_contact_links(self, addresses):
"""Insert 'add to address book' links."""
result = []
title = _("Add to contacts")
url = reverse("api:contact-list")
link_tpl = (
" <a class='addcontact' href='{}' title='{}'>"
"<span class='fa fa-vcard'></span></a>"
)
for address in addresses:
address += link_tpl.format(url, title)
result.append(address)
return result

def fetch_headers(self):
"""Fetch message headers from server."""
msg = self.imapc.fetchmail(
self.mbox, self.mailid, readonly=False,
headers=self.headers_as_list
)
headers = msg["BODY[HEADER.FIELDS ({})]".format(self.headers_as_text)]
self.fetch_body_structure(msg)
msg = email.message_from_string(headers)
contacts_plugin_installed = exts_pool.get_extension("modoboa_contacts")
headers_with_address = ("From", "To", "Cc")
for hdr in self.headernames:
label = hdr[0]
hdrvalue = self.get_header(msg, label)
if not hdrvalue:
continue
if hdr[1]:
if contacts_plugin_installed and label in headers_with_address:
hdrvalue = self._insert_contact_links(hdrvalue)
hdrvalue = ", ".join(hdrvalue)
self.headers += [{"name": label, "value": hdrvalue}]
label = re.sub("-", "_", label)
setattr(self, label, hdrvalue)
Expand All @@ -60,8 +89,6 @@ def get_header(self, msg, hdrname):
We also try to decode the default value.
"""
from . import imapheader

hdrvalue = super(ImapEmail, self).get_header(msg, hdrname)
if not hdrvalue:
return ""
Expand All @@ -74,24 +101,20 @@ def get_header(self, msg, hdrname):
pass
return hdrvalue

@property
def msg(self):
"""
"""
if self._msg is None:
self._msg = self.imapc.fetchmail(
self.mbox, self.mailid, readonly=False,
headers=self.headers_as_list
def fetch_body_structure(self, msg=None):
"""Fetch BODYSTRUCTURE for email."""
if msg is None:
msg = self.imapc.fetchmail(
self.mbox, self.mailid, readonly=False
)
self.bs = BodyStructure(self._msg['BODYSTRUCTURE'])
self._find_attachments()
if self.dformat not in ["plain", "html"]:
self.dformat = self.request.user.parameters.get_value(
self.dformat)
fallback_fmt = "html" if self.dformat == "plain" else "plain"
self.mformat = self.dformat \
if self.dformat in self.bs.contents else fallback_fmt
return self._msg
self.bs = BodyStructure(msg["BODYSTRUCTURE"])
self._find_attachments()
if self.dformat not in ["plain", "html"]:
self.dformat = self.request.user.parameters.get_value(
self.dformat)
fallback_fmt = "html" if self.dformat == "plain" else "plain"
self.mformat = (
self.dformat if self.dformat in self.bs.contents else fallback_fmt)

@property
def headers_as_list(self):
Expand All @@ -107,25 +130,25 @@ def body(self):
This operation has to be made "on demand" because it requires
a communication with the IMAP server.
"""
if self._body is None and self.bs.contents:
bodyc = u''
if self._body is None:
self.fetch_body_structure()
bodyc = u""
for part in self.bs.contents[self.mformat]:
pnum = part['pnum']
pnum = part["pnum"]
data = self.imapc._cmd(
"FETCH", self.mailid, "(BODY.PEEK[%s])" % pnum
)
content = decode_payload(
part['encoding'], data[int(self.mailid)]['BODY[%s]' % pnum]
part["encoding"], data[int(self.mailid)]["BODY[%s]" % pnum]
)
charset = self._find_content_charset(part)
if charset is not None:
try:
content = content.decode(charset)
except (UnicodeDecodeError, LookupError):
result = chardet.detect(content)
content = content.decode(result['encoding'])
content = content.decode(result["encoding"])
bodyc += content
self._fetch_inlines()
self._body = getattr(self, "viewmail_%s" % self.mformat)(
Expand Down Expand Up @@ -193,16 +216,6 @@ def map_cid(self, url):
return self.bs.inlines[m.group(1)]["fname"]
return url

def render_headers(self, **kwargs):
from django.template.loader import render_to_string

res = render_to_string("modoboa_webmail/headers.html", {
"headers": self.headers,
"folder": kwargs["folder"], "mail_id": kwargs["mail_id"],
"attachments": self.attachments != {} and self.attachments or None
})
return res

def fetch_attachment(self, pnum):
"""Fetch an attachment from the IMAP server."""
return self.imapc.fetchpart(self.mailid, self.mbox, pnum)
Expand Down
36 changes: 14 additions & 22 deletions modoboa_webmail/lib/imapheader.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,11 @@


def to_unicode(value):
"""Try to convert a string to unicode.
"""
"""Try to convert a string to unicode."""
if value is None or isinstance(value, unicode):
return value
try:
value = value.decode('utf-8')
value = value.decode("utf-8")
except UnicodeDecodeError:
pass
else:
Expand All @@ -61,39 +60,33 @@ def to_unicode(value):
def parse_address(value, **kwargs):
"""Parse an email address."""
addr = EmailAddress(value)
if kwargs.get("full"):
return to_unicode(addr.fulladdress)
result = addr.name and addr.name or addr.fulladdress
return to_unicode(result)
if addr.name:
return u"<span title={}>{}</span>".format(
to_unicode(addr.address), to_unicode(addr.name))
return to_unicode(addr.address)


def parse_address_list(values, **kwargs):
"""Parse a list of email addresses.
"""
"""Parse a list of email addresses."""
lst = values.split(",")
result = ""
result = []
for addr in lst:
if result != "":
result += ", "
result += parse_address(addr, **kwargs)
result.append(parse_address(addr, **kwargs))
return result


def parse_from(value, **kwargs):
"""Parse a From: header.
"""
return parse_address(value, **kwargs)
"""Parse a From: header."""
return [parse_address(value, **kwargs)]


def parse_to(value, **kwargs):
"""Parse a To: header.
"""
"""Parse a To: header."""
return parse_address_list(value, **kwargs)


def parse_cc(value, **kwargs):
"""Parse a Cc: header.
"""
"""Parse a Cc: header."""
return parse_address_list(value, **kwargs)


Expand Down Expand Up @@ -124,8 +117,7 @@ def parse_message_id(value, **kwargs):


def parse_subject(value, **kwargs):
"""Parse a Subject: header.
"""
"""Parse a Subject: header."""
from modoboa.lib import u2u_decode

try:
Expand Down

0 comments on commit 44618c1

Please sign in to comment.