On Thu, Feb 25, 2021 at 11:50:01AM +0000, Sudip Mukherjee wrote:
> There is a change merged in upstream which should fix this issue.
> Details at: https://github.com/OfflineIMAP/offlineimap3/pull/56
> 
> It will be great if you can test the latest HEAD from github and
> confirm if it fixes the issue.
> I can give you a deb package if that is easier.

I have tested this change and it appears to resolve the issue.  Attached
is the patch that I applied to the current debian/master salsa branch,
which should be suitable for incorporating directly into the package as
is.  I can send it in a salsa MR if you'd like.

> But in any case, I think the change is too big to be included in
> Debian during  this time of the Bullseye freeze. I can add it to
> bullseye-backports after the change has been properly tested.

Respectfully, this is absolutely not the right response.
stable-backports is intended for providing feature updates, and is not
the appropriate place to publish fixes for Severity: important bugs.
This bug has a significant impact on the ability of this package to
fulfill its intended purpose, and further is a regression from buster.
Even if bullseye was already released as stable, this bug would warrant
a fix in a stable point release.  This issue should most definitely be
fixed during the bullseye freeze.

noah

>From 80b2e21783ae8e3348e8952c3055cf2cd3c500ce Mon Sep 17 00:00:00 2001
From: Noah Meyerhans <[email protected]>
Date: Thu, 25 Feb 2021 06:22:03 -0800
Subject: [PATCH] Import upstream fixes for bug 981485

---
 debian/patches/bug981485.patch | 977 +++++++++++++++++++++++++++++++++
 debian/patches/series          |   1 +
 2 files changed, 978 insertions(+)
 create mode 100644 debian/patches/bug981485.patch

diff --git a/debian/patches/bug981485.patch b/debian/patches/bug981485.patch
new file mode 100644
index 0000000..e44696f
--- /dev/null
+++ b/debian/patches/bug981485.patch
@@ -0,0 +1,977 @@
+Index: offlineimap3/offlineimap/emailutil.py
+===================================================================
+--- offlineimap3.orig/offlineimap/emailutil.py
++++ /dev/null
+@@ -1,38 +0,0 @@
+-# Some useful functions to extract data out of emails
+-# Copyright (C) 2002-2015 John Goerzen & contributors
+-#
+-#    This program is free software; you can redistribute it and/or modify
+-#    it under the terms of the GNU General Public License as published by
+-#    the Free Software Foundation; either version 2 of the License, or
+-#    (at your option) any later version.
+-#
+-#    This program is distributed in the hope that it will be useful,
+-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-#    GNU General Public License for more details.
+-#
+-#    You should have received a copy of the GNU General Public License
+-#    along with this program; if not, write to the Free Software
+-#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+-
+-import email
+-from email.parser import Parser as MailParser
+-
+-
+-def get_message_date(content, header='Date'):
+-    """Parses mail and returns resulting timestamp.
+-
+-    :param content: Mail content
+-    :param header: the header to extract date from;
+-    :returns: timestamp or `None` in the case of failure.
+-    """
+-
+-    message = MailParser().parsestr(content, True)
+-    dateheader = message.get(header)
+-    # parsedate_tz returns a 10-tuple that can be passed to mktime_tz
+-    # Will be None if missing or not in a valid format.  Note that
+-    # indexes 6, 7, and 8 of the result tuple are not usable.
+-    datetuple = email.utils.parsedate_tz(dateheader)
+-    if datetuple is None:
+-        return None
+-    return email.utils.mktime_tz(datetuple)
+Index: offlineimap3/offlineimap/folder/Base.py
+===================================================================
+--- offlineimap3.orig/offlineimap/folder/Base.py
++++ offlineimap3/offlineimap/folder/Base.py
+@@ -22,6 +22,11 @@ import re
+ import time
+ from sys import exc_info
+ 
++from email import policy
++from email.parser import BytesParser
++from email.generator import BytesGenerator
++from email.utils import parsedate_tz, mktime_tz
++
+ from offlineimap import threadutil
+ from offlineimap.ui import getglobalui
+ from offlineimap.error import OfflineImapError
+@@ -42,6 +47,22 @@ class BaseFolder:
+ 
+         self.ui = getglobalui()
+         self.messagelist = {}
++        # Use the built-in email libraries
++        # Establish some policies
++        self.policy = {
++          '7bit':
++          policy.default.clone(cte_type='7bit', utf8=False, refold_source='none'),
++          '7bit-RFC':
++          policy.default.clone(cte_type='7bit', utf8=False, refold_source='none', linesep='\r\n'),
++          '8bit':
++          policy.default.clone(cte_type='8bit', utf8=True, refold_source='none'),
++          '8bit-RFC':
++          policy.default.clone(cte_type='8bit', utf8=True, refold_source='none', linesep='\r\n'),
++        }
++        # Parsers
++        self.parser = {}
++        for key in self.policy:
++            self.parser[key] = BytesParser(policy=self.policy[key])
+         # Save original name for folderfilter operations.
+         self.ffilter_name = name
+         # Top level dir name is always ''.
+@@ -349,7 +370,7 @@ class BaseFolder:
+         return len(self.getmessagelist())
+ 
+     def getmessage(self, uid):
+-        """Returns the content of the specified message."""
++        """Returns an email message object."""
+ 
+         raise NotImplementedError
+ 
+@@ -466,7 +487,7 @@ class BaseFolder:
+         except:
+             raise IOError("Can't read %s" % uidfile)
+ 
+-    def savemessage(self, uid, content, flags, rtime):
++    def savemessage(self, uid, msg, flags, rtime):
+         """Writes a new message, with the specified uid.
+ 
+         If the uid is < 0: The backend should assign a new uid and
+@@ -637,211 +658,90 @@ class BaseFolder:
+         for uid in uidlist:
+             self.deletemessagelabels(uid, labels)
+ 
+-    def addmessageheader(self, content, linebreak, headername, headervalue):
++    def addmessageheader(self, msg, headername, headervalue):
+         """Adds new header to the provided message.
+ 
+-        WARNING: This function is a bit tricky, and modifying it in the wrong
+-        way, may easily lead to data-loss.
+-
+         Arguments:
+-        - content: message content, headers and body as a single string
+-        - linebreak: string that carries line ending
++        - msg: message object
+         - headername: name of the header to add
+         - headervalue: value of the header to add
+ 
+-        .. note::
+-
+-           The following documentation will not get displayed correctly after
+-           being processed by Sphinx. View the source of this method to read it.
++        Returns: None
+ 
+-        This has to deal with strange corner cases where the header is
+-        missing or empty.  Here are illustrations for all the cases,
+-        showing where the header gets inserted and what the end result
+-        is.  In each illustration, '+' means the added contents.  Note
+-        that these examples assume LF for linebreak, not CRLF, so '\n'
+-        denotes a linebreak and '\n\n' corresponds to the transition
+-        between header and body.  However if the linebreak parameter
+-        is set to '\r\n' then you would have to substitute '\r\n' for
+-        '\n' in the below examples.
+-
+-          * Case 1: No '\n\n', leading '\n'
+-
+-            +X-Flying-Pig-Header: i am here\n
+-            \n
+-            This is the body\n
+-            next line\n
+-
+-          * Case 2: '\n\n' at position 0
+-
+-            +X-Flying-Pig-Header: i am here
+-            \n
+-            \n
+-            This is the body\n
+-            next line\n
+-
+-          * Case 3: No '\n\n', no leading '\n'
+-
+-            +X-Flying-Pig-Header: i am here\n
+-            +\n
+-            This is the body\n
+-            next line\n
+-
+-          * Case 4: '\n\n' at non-zero position
+-
+-            Subject: Something wrong with OI\n
+-            From: [email protected]
+-            +\nX-Flying-Pig-Header: i am here
+-            \n
+-            \n
+-            This is the body\n
+-            next line\n
+         """
+ 
+         self.ui.debug('', 'addmessageheader: called to add %s: %s' %
+                       (headername, headervalue))
+ 
+-        insertionpoint = content.find(linebreak * 2)
+-        if insertionpoint == -1:
+-            self.ui.debug('', 'addmessageheader: headers were missing')
+-        else:
+-            self.ui.debug('',
+-                          'addmessageheader: headers end at position %d' %
+-                          insertionpoint)
+-            mark = '==>EOH<=='
+-            contextstart = max(0, insertionpoint - 100)
+-            contextend = min(len(content), insertionpoint + 100)
+-            self.ui.debug('', 'addmessageheader: header/body transition " \
+-                "context (marked by %s): %s%s%s' % (
+-                mark, repr(content[contextstart:insertionpoint]),
+-                mark, repr(content[insertionpoint:contextend])
+-            )
+-                          )
+-
+-        # Hoping for case #4.
+-        prefix = linebreak
+-        suffix = ''
+-        # Case #2.
+-        if insertionpoint == 0:
+-            prefix = ''
+-            suffix = ''
+-        # Either case #1 or #3.
+-        elif insertionpoint == -1:
+-            prefix = ''
+-            suffix = linebreak
+-            insertionpoint = 0
+-            # Case #3: when body starts immediately, without preceding '\n'
+-            # (this shouldn't happen with proper mail messages, but
+-            # we seen many broken ones), we should add '\n' to make
+-            # new (and the only header, in this case) to be properly
+-            # separated from the message body.
+-            if content[0:len(linebreak)] != linebreak:
+-                suffix = suffix + linebreak
+-
+-        self.ui.debug('',
+-                      'addmessageheader: insertionpoint = %d' % insertionpoint)
+-        headers = content[0:insertionpoint]
+-        self.ui.debug('',
+-                      'addmessageheader: headers = %s' % repr(headers))
+-        new_header = prefix + ("%s: %s" % (headername, headervalue)) + suffix
+-        self.ui.debug('',
+-                      'addmessageheader: new_header = %s' % repr(new_header))
+-        return headers + new_header + content[insertionpoint:]
+-
+-    def __find_eoh(self, content):
+-        """Searches for the point where mail headers end.
+-
+-        Either double '\n', or end of string.
+-
+-        Arguments:
+-        - content: contents of the message to search in
+-        Returns: position of the first non-header byte.
+-        """
+-
+-        eoh_cr = content.find('\n\n')
+-        if eoh_cr == -1:
+-            eoh_cr = len(content)
++        msg.add_header(headername, headervalue)
++        return
+ 
+-        return eoh_cr
+-
+-    def getmessageheader(self, content, name):
+-        """Return the value of the first occurence of the given header.
++    def getmessageheader(self, msg, headername):
++        """Return the value of an undefined occurence of the given header.
+ 
+         Header name is case-insensitive.
+ 
+         Arguments:
+-        - contents: message itself
+-        - name: name of the header to be searched
++        - msg: message object
++        - headername: name of the header to be searched
+ 
+         Returns: header value or None if no such header was found.
+         """
+ 
+-        self.ui.debug('', 'getmessageheader: called to get %s' % name)
+-        eoh = self.__find_eoh(content)
+-        self.ui.debug('', 'getmessageheader: eoh = %d' % eoh)
+-        headers = content[0:eoh]
+-        self.ui.debug('', 'getmessageheader: headers = %s' % repr(headers))
+-
+-        m = re.search('^%s:(.*)$' % name, headers,
+-                      flags=re.MULTILINE | re.IGNORECASE)
+-        if m:
+-            return m.group(1).strip()
+-        else:
+-            return None
++        self.ui.debug('', 'getmessageheader: called to get %s' % headername)
++        return msg.get(headername)
+ 
+-    def getmessageheaderlist(self, content, name):
++    def getmessageheaderlist(self, msg, headername):
+         """Return a list of values for the given header.
+ 
++        Header name is case-insensitive.
++
+         Arguments:
+-        - contents: message itself
+-        - name: name of the header to be searched
++        - msg: message object
++        - headername: name of the header to be searched
+ 
+         Returns: list of header values or empty list if no such header was
+         found.
+         """
+ 
+-        self.ui.debug('', 'getmessageheaderlist: called to get %s' % name)
+-        eoh = self.__find_eoh(content)
+-        self.ui.debug('', 'getmessageheaderlist: eoh = %d' % eoh)
+-        headers = content[0:eoh]
+-        self.ui.debug('', 'getmessageheaderlist: headers = %s' % repr(headers))
+-
+-        return re.findall('^%s:(.*)$' %
+-                          name, headers, flags=re.MULTILINE | re.IGNORECASE)
++        self.ui.debug('', 'getmessageheaderlist: called to get %s' % headername)
++        return msg.get_all(headername, [])
+ 
+-    def deletemessageheaders(self, content, header_list):
+-        """Deletes headers in the given list from the message content.
++    def deletemessageheaders(self, msg, header_list):
++        """Deletes headers in the given list from the message.
+ 
+         Arguments:
+-        - content: message itself
++        - msg: message object
+         - header_list: list of headers to be deleted or just the header name
+ 
+-        We expect our message to have '\n' as line endings."""
++        """
+ 
+         if type(header_list) != type([]):
+             header_list = [header_list]
+         self.ui.debug('',
+                       'deletemessageheaders: called to delete %s' % header_list)
+ 
+-        if not len(header_list):
+-            return content
++        for h in header_list:
++            del msg[h]
+ 
+-        eoh = self.__find_eoh(content)
+-        self.ui.debug('', 'deletemessageheaders: end of headers = %d' % eoh)
+-        headers = content[0:eoh]
+-        rest = content[eoh:]
+-        self.ui.debug('', 'deletemessageheaders: headers = %s' % repr(headers))
+-        new_headers = []
+-        for h in headers.split('\n'):
+-            keep_it = True
+-            for trim_h in header_list:
+-                if len(h) > len(trim_h) \
+-                        and h[0:len(trim_h) + 1] == (trim_h + ":"):
+-                    keep_it = False
+-                    break
+-            if keep_it:
+-                new_headers.append(h)
++        return
++
++    def get_message_date(self, msg, header="Date"):
++        """Returns the Unix timestamp of the email message, derived from the
++        Date field header by default.
++
++        Arguments:
++        - msg: message object
++        - header: header to extract the date from 
++
++        Returns: timestamp or `None` in the case of failure.
++        """
++
++        datetuple = parsedate_tz(msg.get(header))
++        if datetuple is None:
++            return None
+ 
+-        return '\n'.join(new_headers) + rest
++        return mktime_tz(datetuple)
+ 
+     def change_message_uid(self, uid, new_uid):
+         """Change the message from existing uid to new_uid.
+Index: offlineimap3/offlineimap/folder/Gmail.py
+===================================================================
+--- offlineimap3.orig/offlineimap/folder/Gmail.py
++++ offlineimap3/offlineimap/folder/Gmail.py
+@@ -69,14 +69,13 @@ class GmailFolder(IMAPFolder):
+         data = self._fetch_from_imap(str(uid), self.retrycount)
+ 
+         # data looks now e.g.
+-        # ['320 (X-GM-LABELS (...) UID 17061 BODY[] {2565}','msgbody....']
++        # ['320 (X-GM-LABELS (...) UID 17061 BODY[] {2565}',<email.message.EmailMessage object>]
+         # we only asked for one message, and that msg is in data[1].
+-        # msbody is in [1].
+-        body = data[1].replace("\r\n", "\n")
++        msg = data[1]
+ 
+         # Embed the labels into the message headers
+         if self.synclabels:
+-            m = re.search('X-GM-LABELS\s*[(](.*)[)]', data[0][0])
++            m = re.search('X-GM-LABELS\s*[(](.*)[)]', data[0])
+             if m:
+                 labels = set([imaputil.dequote(lb) for lb in imaputil.imapsplit(m.group(1))])
+             else:
+@@ -84,19 +83,23 @@ class GmailFolder(IMAPFolder):
+             labels = labels - self.ignorelabels
+             labels_str = imaputil.format_labels_string(self.labelsheader, sorted(labels))
+ 
+-            # First remove old label headers that may be in the message content retrieved
++            # First remove old label headers that may be in the message body retrieved
+             # from gmail Then add a labels header with current gmail labels.
+-            body = self.deletemessageheaders(body, self.labelsheader)
+-            body = self.addmessageheader(body, '\n', self.labelsheader, labels_str)
++            self.deletemessageheaders(msg, self.labelsheader)
++            self.addmessageheader(msg, self.labelsheader, labels_str)
+ 
+-        if len(body) > 200:
+-            dbg_output = "%s...%s" % (str(body)[:150], str(body)[-50:])
+-        else:
+-            dbg_output = body
++        if self.ui.is_debugging('imap'):
++            # Optimization: don't create the debugging objects unless needed
++            msg_s = msg.as_string(policy=self.policy['8bit-RFC'])
++            if len(msg_s) > 200:
++                dbg_output = "%s...%s" % (msg_s[:150], msg_s[-50:])
++            else:
++                dbg_output = msg_s
++
++            self.ui.debug('imap', "Returned object from fetching %d: '%s'" %
++                          (uid, dbg_output))
+ 
+-        self.ui.debug('imap', "Returned object from fetching %d: '%s'" %
+-                      (uid, dbg_output))
+-        return body
++        return msg
+ 
+     def getmessagelabels(self, uid):
+         if 'labels' in self.messagelist[uid]:
+@@ -171,7 +174,7 @@ class GmailFolder(IMAPFolder):
+                 rtime = imaplibutil.Internaldate2epoch(messagestr)
+                 self.messagelist[uid] = {'uid': uid, 'flags': flags, 'labels': labels, 'time': rtime}
+ 
+-    def savemessage(self, uid, content, flags, rtime):
++    def savemessage(self, uid, msg, flags, rtime):
+         """Save the message on the Server
+ 
+         This backend always assigns a new uid, so the uid arg is ignored.
+@@ -184,7 +187,7 @@ class GmailFolder(IMAPFolder):
+         savemessage is never called in a dryrun mode.
+ 
+         :param uid: Message UID
+-        :param content: Message content
++        :param msg: Message object
+         :param flags: Message flags
+         :param rtime: A timestamp to be used as the mail date
+         :returns: the UID of the new message as assigned by the server. If the
+@@ -193,13 +196,13 @@ class GmailFolder(IMAPFolder):
+                   read-only for example) it will return -1."""
+ 
+         if not self.synclabels:
+-            return super(GmailFolder, self).savemessage(uid, content, flags, rtime)
++            return super(GmailFolder, self).savemessage(uid, msg, flags, rtime)
+ 
+         labels = set()
+-        for hstr in self.getmessageheaderlist(content, self.labelsheader):
++        for hstr in self.getmessageheaderlist(msg, self.labelsheader):
+             labels.update(imaputil.labels_from_header(self.labelsheader, hstr))
+ 
+-        ret = super(GmailFolder, self).savemessage(uid, content, flags, rtime)
++        ret = super(GmailFolder, self).savemessage(uid, msg, flags, rtime)
+         self.savemessagelabels(ret, labels)
+         return ret
+ 
+Index: offlineimap3/offlineimap/folder/GmailMaildir.py
+===================================================================
+--- offlineimap3.orig/offlineimap/folder/GmailMaildir.py
++++ offlineimap3/offlineimap/folder/GmailMaildir.py
+@@ -90,12 +90,12 @@ class GmailMaildirFolder(MaildirFolder):
+             if not os.path.exists(filepath):
+                 return set()
+ 
+-            file = open(filepath, 'rt')
+-            content = file.read()
+-            file.close()
++            fd = open(filepath, 'rb')
++            msg = self.parser['8bit'].parse(fd)
++            fd.close()
+ 
+             self.messagelist[uid]['labels'] = set()
+-            for hstr in self.getmessageheaderlist(content, self.labelsheader):
++            for hstr in self.getmessageheaderlist(msg, self.labelsheader):
+                 self.messagelist[uid]['labels'].update(
+                     imaputil.labels_from_header(self.labelsheader, hstr))
+             self.messagelist[uid]['labels_cached'] = True
+@@ -108,7 +108,7 @@ class GmailMaildirFolder(MaildirFolder):
+         else:
+             return self.messagelist[uid]['mtime']
+ 
+-    def savemessage(self, uid, content, flags, rtime):
++    def savemessage(self, uid, msg, flags, rtime):
+         """Writes a new message, with the specified uid.
+ 
+         See folder/Base for detail. Note that savemessage() does not
+@@ -116,14 +116,15 @@ class GmailMaildirFolder(MaildirFolder):
+         savemessage is never called in a dryrun mode."""
+ 
+         if not self.synclabels:
+-            return super(GmailMaildirFolder, self).savemessage(uid, content,
++            return super(GmailMaildirFolder, self).savemessage(uid, msg,
+                                                                flags, rtime)
+ 
+         labels = set()
+-        for hstr in self.getmessageheaderlist(content, self.labelsheader):
++        for hstr in self.getmessageheaderlist(msg, self.labelsheader):
+             labels.update(imaputil.labels_from_header(self.labelsheader, hstr))
+ 
+-        ret = super(GmailMaildirFolder, self).savemessage(uid, content, flags,
++        # TODO - Not sure why the returned uid is stored early as ret here?
++        ret = super(GmailMaildirFolder, self).savemessage(uid, msg, flags,
+                                                           rtime)
+ 
+         # Update the mtime and labels.
+@@ -145,12 +146,12 @@ class GmailMaildirFolder(MaildirFolder):
+         filename = self.messagelist[uid]['filename']
+         filepath = os.path.join(self.getfullname(), filename)
+ 
+-        file = open(filepath, 'rt')
+-        content = file.read()
+-        file.close()
++        fd = open(filepath, 'rb')
++        msg = self.parser['8bit'].parse(fd)
++        fd.close()
+ 
+         oldlabels = set()
+-        for hstr in self.getmessageheaderlist(content, self.labelsheader):
++        for hstr in self.getmessageheaderlist(msg, self.labelsheader):
+             oldlabels.update(imaputil.labels_from_header(self.labelsheader,
+                                                          hstr))
+ 
+@@ -167,15 +168,14 @@ class GmailMaildirFolder(MaildirFolder):
+                                                    sorted(labels | ignoredlabels))
+ 
+         # First remove old labels header, and then add the new one.
+-        content = self.deletemessageheaders(content, self.labelsheader)
+-        content = self.addmessageheader(content, '\n', self.labelsheader,
+-                                        labels_str)
++        self.deletemessageheaders(msg, self.labelsheader)
++        self.addmessageheader(msg, self.labelsheader, labels_str)
+ 
+         mtime = int(os.stat(filepath).st_mtime)
+ 
+         # Write file with new labels to a unique file in tmp.
+         messagename = self.new_message_filename(uid, set())
+-        tmpname = self.save_to_tmp_file(messagename, content)
++        tmpname = self.save_to_tmp_file(messagename, msg)
+         tmppath = os.path.join(self.getfullname(), tmpname)
+ 
+         # Move to actual location.
+Index: offlineimap3/offlineimap/folder/IMAP.py
+===================================================================
+--- offlineimap3.orig/offlineimap/folder/IMAP.py
++++ offlineimap3/offlineimap/folder/IMAP.py
+@@ -20,7 +20,7 @@ import binascii
+ import re
+ import time
+ from sys import exc_info
+-from offlineimap import imaputil, imaplibutil, emailutil, OfflineImapError
++from offlineimap import imaputil, imaplibutil, OfflineImapError
+ from offlineimap import globals
+ from imaplib2 import MonthNames
+ from .Base import BaseFolder
+@@ -30,13 +30,6 @@ CRLF = '\r\n'
+ MSGCOPY_NAMESPACE = 'MSGCOPY_'
+ 
+ 
+-# NB: message returned from getmessage() will have '\n' all over the place,
+-# NB: there will be no CRLFs.  Just before the sending stage of savemessage()
+-# NB: '\n' will be transformed back to CRLF.  So, for the most parts of the
+-# NB: code the stored content will be clean of CRLF and one can rely that
+-# NB: line endings will be pure '\n'.
+-
+-
+ class IMAPFolder(BaseFolder):
+     def __init__(self, imapserver, name, repository, decode=True):
+         # decode the folder name from IMAP4_utf_7 to utf_8 if
+@@ -349,19 +342,22 @@ class IMAPFolder(BaseFolder):
+         data = self._fetch_from_imap(str(uid), self.retrycount)
+ 
+         # Data looks now e.g.
+-        # ['320 (17061 BODY[] {2565}','msgbody....']
++        # ['320 (17061 BODY[] {2565}',<email.message.EmailMessage object>]
+         # Is a list of two elements. Message is at [1]
+-        data = data[1].replace(CRLF, "\n")
++        msg = data[1]
+ 
+-        if len(data) > 200:
+-            dbg_output = "%s...%s" % (str(data)[:150], str(data)[-50:])
+-        else:
+-            dbg_output = data
++        if self.ui.is_debugging('imap'):
++            # Optimization: don't create the debugging objects unless needed
++            msg_s = msg.as_string(policy=self.policy['8bit-RFC'])
++            if len(msg_s) > 200:
++                dbg_output = "%s...%s" % (msg_s[:150], msg_s[-50:])
++            else:
++                dbg_output = msg_s
+ 
+-        self.ui.debug('imap', "Returned object from fetching %d: '%s'" %
+-                      (uid, dbg_output))
++            self.ui.debug('imap', "Returned object from fetching %d: '%s'" %
++                          (uid, dbg_output))
+ 
+-        return data
++        return msg
+ 
+     # Interface from BaseFolder
+     def getmessagetime(self, uid):
+@@ -375,7 +371,7 @@ class IMAPFolder(BaseFolder):
+     def getmessagekeywords(self, uid):
+         return self.messagelist[uid]['keywords']
+ 
+-    def __generate_randomheader(self, content):
++    def __generate_randomheader(self, msg, policy=None):
+         """Returns a unique X-OfflineIMAP header
+ 
+          Generate an 'X-OfflineIMAP' mail header which contains a random
+@@ -390,17 +386,21 @@ class IMAPFolder(BaseFolder):
+         """
+ 
+         headername = 'X-OfflineIMAP'
++        if policy is None:
++            output_policy = self.policy['8bit-RFC']
++        else:
++            output_policy = policy
+         # We need a random component too. If we ever upload the same
+         # mail twice (e.g. in different folders), we would still need to
+         # get the UID for the correct one. As we won't have too many
+         # mails with identical content, the randomness requirements are
+         # not extremly critial though.
+ 
+-        # Compute unsigned crc32 of 'content' as unique hash.
++        # Compute unsigned crc32 of 'msg' (as bytes) into a unique hash.
+         # NB: crc32 returns unsigned only starting with python 3.0.
+-        headervalue = str(binascii.crc32(str.encode(content))
+-                          & 0xffffffff) + '-'
+-        headervalue += str(self.randomgenerator.randint(0, 9999999999))
++        headervalue = '{}-{}'.format(
++          (binascii.crc32(msg.as_bytes(policy=output_policy)) & 0xffffffff),
++          self.randomgenerator.randint(0, 9999999999))
+         return headername, headervalue
+ 
+     def __savemessage_searchforheader(self, imapobj, headername, headervalue):
+@@ -539,7 +539,7 @@ class IMAPFolder(BaseFolder):
+ 
+         return 0
+ 
+-    def __getmessageinternaldate(self, content, rtime=None):
++    def __getmessageinternaldate(self, msg, rtime=None):
+         """Parses mail and returns an INTERNALDATE string
+ 
+         It will use information in the following order, falling back as an
+@@ -571,7 +571,7 @@ class IMAPFolder(BaseFolder):
+                   (which is fine as value for append)."""
+ 
+         if rtime is None:
+-            rtime = emailutil.get_message_date(content)
++            rtime = self.get_message_date(msg)
+             if rtime is None:
+                 return None
+         datetuple = time.localtime(rtime)
+@@ -619,7 +619,7 @@ class IMAPFolder(BaseFolder):
+         return internaldate
+ 
+     # Interface from BaseFolder
+-    def savemessage(self, uid, content, flags, rtime):
++    def savemessage(self, uid, msg, flags, rtime):
+         """Save the message on the Server
+ 
+         This backend always assigns a new uid, so the uid arg is ignored.
+@@ -632,7 +632,7 @@ class IMAPFolder(BaseFolder):
+         savemessage is never called in a dryrun mode.
+ 
+         :param uid: Message UID
+-        :param content: Message content
++        :param msg: Message Object
+         :param flags: Message flags
+         :param rtime: A timestamp to be used as the mail date
+         :returns: the UID of the new message as assigned by the server. If the
+@@ -647,16 +647,17 @@ class IMAPFolder(BaseFolder):
+             self.savemessageflags(uid, flags)
+             return uid
+ 
+-        content = self.deletemessageheaders(content, self.filterheaders)
++        # Filter user requested headers before uploading to the IMAP server
++        self.deletemessageheaders(msg, self.filterheaders)
+ 
+-        # Use proper CRLF all over the message.
+-        content = re.sub("(?<!\r)\n", CRLF, content)
++        # Should just be able to set the policy, to use CRLF in msg output
++        output_policy = self.policy['8bit-RFC']
+ 
+         # Get the date of the message, so we can pass it to the server.
+-        date = self.__getmessageinternaldate(content, rtime)
++        date = self.__getmessageinternaldate(msg, rtime)
+ 
+         # Message-ID is handy for debugging messages.
+-        msg_id = self.getmessageheader(content, "message-id")
++        msg_id = self.getmessageheader(msg, "message-id")
+         if not msg_id:
+             msg_id = '[unknown message-id]'
+ 
+@@ -676,18 +677,20 @@ class IMAPFolder(BaseFolder):
+                 if not use_uidplus:
+                     # Insert a random unique header that we can fetch later.
+                     (headername, headervalue) = self.__generate_randomheader(
+-                        content)
++                        msg)
+                     self.ui.debug('imap', 'savemessage: header is: %s: %s' %
+                                   (headername, headervalue))
+-                    content = self.addmessageheader(content, CRLF,
+-                                                    headername, headervalue)
++                    self.addmessageheader(msg, headername, headervalue)
+ 
+-                if len(content) > 200:
+-                    dbg_output = "%s...%s" % (content[:150], content[-50:])
+-                else:
+-                    dbg_output = content
+-                self.ui.debug('imap', "savemessage: date: %s, content: '%s'" %
+-                              (date, dbg_output))
++                if self.ui.is_debugging('imap'):
++                    # Optimization: don't create the debugging objects unless needed
++                    msg_s = msg.as_string(policy=output_policy)
++                    if len(msg_s) > 200:
++                        dbg_output = "%s...%s" % (msg_s[:150], msg_s[-50:])
++                    else:
++                        dbg_output = msg_s
++                    self.ui.debug('imap', "savemessage: date: %s, content: '%s'" %
++                                  (date, dbg_output))
+ 
+                 try:
+                     # Select folder for append and make the box READ-WRITE.
+@@ -695,7 +698,7 @@ class IMAPFolder(BaseFolder):
+                 except imapobj.readonly:
+                     # readonly exception. Return original uid to notify that
+                     # we did not save the message. (see savemessage in Base.py)
+-                    self.ui.msgtoreadonly(self, uid, content, flags)
++                    self.ui.msgtoreadonly(self, uid)
+                     return uid
+ 
+                 # Do the APPEND.
+@@ -703,7 +706,7 @@ class IMAPFolder(BaseFolder):
+                     (typ, dat) = imapobj.append(
+                         self.getfullIMAPname(),
+                         imaputil.flagsmaildir2imap(flags),
+-                        date,  bytes(content, 'utf-8'))
++                        date,  msg.as_bytes(policy=output_policy))
+                     # This should only catch 'NO' responses since append()
+                     # will raise an exception for 'BAD' responses:
+                     if typ != 'OK':
+@@ -716,12 +719,12 @@ class IMAPFolder(BaseFolder):
+                         # In this case, we should immediately abort
+                         # the repository sync and continue
+                         # with the next account.
+-                        msg = \
++                        err_msg = \
+                             "Saving msg (%s) in folder '%s', " \
+                             "repository '%s' failed (abort). " \
+                             "Server responded: %s %s\n" % \
+                             (msg_id, self, self.getrepository(), typ, dat)
+-                        raise OfflineImapError(msg, OfflineImapError.ERROR.REPO)
++                        raise OfflineImapError(err_msg, OfflineImapError.ERROR.REPO)
+                     retry_left = 0  # Mark as success.
+                 except imapobj.abort as e:
+                     # Connection has been reset, release connection and retry.
+@@ -832,7 +835,7 @@ class IMAPFolder(BaseFolder):
+         """Fetches data from IMAP server.
+ 
+         Arguments:
+-        - uids: message UIDS
++        - uids: message UIDS (OfflineIMAP3: First UID returned only)
+         - retry_num: number of retries to make
+ 
+         Returns: data obtained by this query."""
+@@ -888,9 +891,21 @@ class IMAPFolder(BaseFolder):
+                          "with UID '%s'" % (self.getrepository(), uids)
+             raise OfflineImapError(reason, severity)
+ 
+-        # Convert bytes to str
++        # JI: In offlineimap, this function returned a tuple of strings for each
++        # fetched UID, offlineimap3 calls to the imap object return bytes and so
++        # originally a fixed, utf-8 conversion was done and *only* the first
++        # response (d[0]) was returned.  Note that this alters the behavior
++        # between code bases.  However, it seems like a single UID is the intent
++        # of this function so retaining the modfication here for now.
++        # 
++        # TODO: Can we assume the server response containing the meta data is
++        # always 'utf-8' encoded?  Assuming yes for now.
++        #
++        # Convert responses, d[0][0], into a 'utf-8' string (from bytes) and
++        # Convert email, d[0][1], into a message object (from bytes) 
++
+         ndata0 = data[0][0].decode('utf-8')
+-        ndata1 = data[0][1].decode('utf-8', errors='replace')
++        ndata1 = self.parser['8bit-RFC'].parsebytes(data[0][1])
+         ndata = [ndata0, ndata1]
+ 
+         return ndata
+Index: offlineimap3/offlineimap/folder/LocalStatus.py
+===================================================================
+--- offlineimap3.orig/offlineimap/folder/LocalStatus.py
++++ offlineimap3/offlineimap/folder/LocalStatus.py
+@@ -190,7 +190,7 @@ class LocalStatusFolder(BaseFolder):
+                 os.close(fd)
+ 
+     # Interface from BaseFolder
+-    def savemessage(self, uid, content, flags, rtime, mtime=0, labels=None):
++    def savemessage(self, uid, msg, flags, rtime, mtime=0, labels=None):
+         """Writes a new message, with the specified uid.
+ 
+         See folder/Base for detail. Note that savemessage() does not
+Index: offlineimap3/offlineimap/folder/LocalStatusSQLite.py
+===================================================================
+--- offlineimap3.orig/offlineimap/folder/LocalStatusSQLite.py
++++ offlineimap3/offlineimap/folder/LocalStatusSQLite.py
+@@ -323,7 +323,7 @@ class LocalStatusSQLiteFolder(BaseFolder
+     #        assert False,"getmessageflags() called on non-existing message"
+ 
+     # Interface from BaseFolder
+-    def savemessage(self, uid, content, flags, rtime, mtime=0, labels=None):
++    def savemessage(self, uid, msg, flags, rtime, mtime=0, labels=None):
+         """Writes a new message, with the specified uid.
+ 
+         See folder/Base for detail. Note that savemessage() does not
+Index: offlineimap3/offlineimap/folder/Maildir.py
+===================================================================
+--- offlineimap3.orig/offlineimap/folder/Maildir.py
++++ offlineimap3/offlineimap/folder/Maildir.py
+@@ -23,7 +23,7 @@ import os
+ from sys import exc_info
+ from threading import Lock
+ from hashlib import md5
+-from offlineimap import OfflineImapError, emailutil
++from offlineimap import OfflineImapError
+ from .Base import BaseFolder
+ 
+ # Find the UID in a message filename
+@@ -254,16 +254,14 @@ class MaildirFolder(BaseFolder):
+ 
+     # Interface from BaseFolder
+     def getmessage(self, uid):
+-        """Return the content of the message."""
++        """Returns an email message object."""
+ 
+         filename = self.messagelist[uid]['filename']
+         filepath = os.path.join(self.getfullname(), filename)
+-        file = open(filepath, 'rt')
+-        retval = file.read()
+-        file.close()
+-        # TODO: WHY are we replacing \r\n with \n here? And why do we
+-        #      read it as text?
+-        return retval.replace("\r\n", "\n")
++        fd = open(filepath, 'rb')
++        retval = self.parser['8bit'].parse(fd)
++        fd.close()
++        return retval
+ 
+     # Interface from BaseFolder
+     def getmessagetime(self, uid):
+@@ -288,17 +286,21 @@ class MaildirFolder(BaseFolder):
+                      uid, self._foldermd5, self.infosep, ''.join(sorted(flags)))
+         return uniq_name.replace(os.path.sep, self.sep_subst)
+ 
+-    def save_to_tmp_file(self, filename, content):
+-        """Saves given content to the named temporary file in the
++    def save_to_tmp_file(self, filename, msg, policy=None):
++        """Saves given message to the named temporary file in the
+         'tmp' subdirectory of $CWD.
+ 
+         Arguments:
+         - filename: name of the temporary file;
+-        - content: data to be saved.
++        - msg: Email message object
+ 
+         Returns: relative path to the temporary file
+         that was created."""
+ 
++        if policy is None:
++            output_policy = self.policy['8bit']
++        else:
++            output_policy = policy
+         tmpname = os.path.join('tmp', filename)
+         # Open file and write it out.
+         # XXX: why do we need to loop 7 times?
+@@ -324,8 +326,8 @@ class MaildirFolder(BaseFolder):
+                 else:
+                     raise
+ 
+-        fd = os.fdopen(fd, 'wt')
+-        fd.write(content)
++        fd = os.fdopen(fd, 'wb')
++        fd.write(msg.as_bytes(policy=output_policy))
+         # Make sure the data hits the disk.
+         fd.flush()
+         if self.dofsync():
+@@ -335,7 +337,7 @@ class MaildirFolder(BaseFolder):
+         return tmpname
+ 
+     # Interface from BaseFolder
+-    def savemessage(self, uid, content, flags, rtime):
++    def savemessage(self, uid, msg, flags, rtime):
+         """Writes a new message, with the specified uid.
+ 
+         See folder/Base for detail. Note that savemessage() does not
+@@ -359,15 +361,15 @@ class MaildirFolder(BaseFolder):
+         message_timestamp = None
+         if self._filename_use_mail_timestamp is not False:
+             try:
+-                message_timestamp = emailutil.get_message_date(content, 'Date')
++                message_timestamp = self.get_message_date(msg, 'Date')
+                 if message_timestamp is None:
+                     # Give a try with Delivery-date
+-                    message_timestamp = emailutil.get_message_date(
+-                        content, 'Delivery-date')
++                    message_timestamp = self.get_message_date(
++                        msg, 'Delivery-date')
+             except Exception as e:
+                 # This should never happen.
+                 from offlineimap.ui import getglobalui
+-                datestr = emailutil.get_message_date(content)
++                datestr = self.get_message_date(msg)
+                 ui = getglobalui()
+                 ui.warn("UID %d has invalid date %s: %s\n"
+                         "Not using message timestamp as file prefix" %
+@@ -375,11 +377,11 @@ class MaildirFolder(BaseFolder):
+                 # No need to check if message_timestamp is None here since it
+                 # would be overridden by _gettimeseq.
+         messagename = self.new_message_filename(uid, flags, date=message_timestamp)
+-        tmpname = self.save_to_tmp_file(messagename, content)
++        tmpname = self.save_to_tmp_file(messagename, msg)
+ 
+         if self._utime_from_header is True:
+             try:
+-                date = emailutil.get_message_date(content, 'Date')
++                date = self.get_message_date(msg, 'Date')
+                 if date is not None:
+                     os.utime(os.path.join(self.getfullname(), tmpname),
+                              (date, date))
+@@ -387,7 +389,7 @@ class MaildirFolder(BaseFolder):
+             # int32.
+             except Exception as e:
+                 from offlineimap.ui import getglobalui
+-                datestr = emailutil.get_message_date(content)
++                datestr = self.get_message_date(msg)
+                 ui = getglobalui()
+                 ui.warn("UID %d has invalid date %s: %s\n"
+                         "Not changing file modification time" % (uid, datestr, e))
+Index: offlineimap3/offlineimap/folder/UIDMaps.py
+===================================================================
+--- offlineimap3.orig/offlineimap/folder/UIDMaps.py
++++ offlineimap3/offlineimap/folder/UIDMaps.py
+@@ -236,11 +236,11 @@ class MappedIMAPFolder(IMAPFolder):
+ 
+     # Interface from BaseFolder
+     def getmessage(self, uid):
+-        """Returns the content of the specified message."""
++        """Returns the specified message."""
+         return self._mb.getmessage(self.r2l[uid])
+ 
+     # Interface from BaseFolder
+-    def savemessage(self, uid, content, flags, rtime):
++    def savemessage(self, uid, msg, flags, rtime):
+         """Writes a new message, with the specified uid.
+ 
+         The UIDMaps class will not return a newly assigned uid, as it
+@@ -271,7 +271,7 @@ class MappedIMAPFolder(IMAPFolder):
+             self.savemessageflags(uid, flags)
+             return uid
+ 
+-        newluid = self._mb.savemessage(-1, content, flags, rtime)
++        newluid = self._mb.savemessage(-1, msg, flags, rtime)
+         if newluid < 1:
+             raise OfflineImapError("server of repository '%s' did not return "
+                                    "a valid UID (got '%s') for UID '%s' from '%s'" % (
+Index: offlineimap3/offlineimap/ui/UIBase.py
+===================================================================
+--- offlineimap3.orig/offlineimap/ui/UIBase.py
++++ offlineimap3/offlineimap/ui/UIBase.py
+@@ -231,6 +231,9 @@ class UIBase:
+         else:
+             self.invaliddebug(debugtype)
+ 
++    def is_debugging(self, debugtype):
++        return (debugtype in self.debuglist)
++
+     def debugging(self, debugtype):
+         global debugtypes
+         self.logger.debug("Now debugging for %s: %s" % (debugtype,
+@@ -266,7 +269,7 @@ class UIBase:
+                           (self.getnicename(x), x.getname()) for x in folder_list])
+ 
+     # WARNINGS
+-    def msgtoreadonly(self, destfolder, uid, content, flags):
++    def msgtoreadonly(self, destfolder, uid):
+         if self.config.has_option('general', 'ignore-readonly') and \
+                 self.config.getboolean('general', 'ignore-readonly'):
+             return
diff --git a/debian/patches/series b/debian/patches/series
index f5f5d50..1e48ef9 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -3,3 +3,4 @@ Do-not-use-the-Internet-to-fetch-DTD.patch
 0001-Revert-Use-system-sslcacertfile-by-default.patch
 0001-BUG-Right-format-for-password-from-Curses.patch
 0001-BUG-Gmail-FETCH-error-with-synclabels-enabled.patch
+bug981485.patch
-- 
2.30.0

Reply via email to