Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package b4 for openSUSE:Factory checked in 
at 2023-01-24 19:43:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/b4 (Old)
 and      /work/SRC/openSUSE:Factory/.b4.new.32243 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "b4"

Tue Jan 24 19:43:28 2023 rev:31 rq:1060560 version:0.12.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/b4/b4.changes    2023-01-06 17:06:44.800582678 
+0100
+++ /work/SRC/openSUSE:Factory/.b4.new.32243/b4.changes 2023-01-24 
20:17:41.379670589 +0100
@@ -1,0 +2,7 @@
+Tue Jan 24 06:21:58 UTC 2023 - Jiri Slaby <jsl...@suse.cz>
+
+- update to 0.12.0
+  * worked around Python's email bugs
+  * other bugfixes
+
+-------------------------------------------------------------------

Old:
----
  b4-0.11.2.tar.gz

New:
----
  b4-0.12.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ b4.spec ++++++
--- /var/tmp/diff_new_pack.nEcyMv/_old  2023-01-24 20:17:41.823672890 +0100
+++ /var/tmp/diff_new_pack.nEcyMv/_new  2023-01-24 20:17:41.831672932 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           b4
-Version:        0.11.2
+Version:        0.12.0
 Release:        0
 Summary:        Helper scripts for kernel.org patches
 License:        GPL-2.0-or-later

++++++ b4-0.11.2.tar.gz -> b4-0.12.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/b4-0.11.2/.keys/openpgp/tessares.net/matthieu.baerts/default 
new/b4-0.12.0/.keys/openpgp/tessares.net/matthieu.baerts/default
--- old/b4-0.11.2/.keys/openpgp/tessares.net/matthieu.baerts/default    
1970-01-01 01:00:00.000000000 +0100
+++ new/b4-0.12.0/.keys/openpgp/tessares.net/matthieu.baerts/default    
2023-01-20 16:31:00.000000000 +0100
@@ -0,0 +1,124 @@
+pub   rsa4096 2015-08-31 [SC]
+      E8CB85F76877057A6E27F77AF6B7824F4269A073
+uid                      Matthieu Baerts <matthieu.bae...@tessares.net>
+uid                      Matthieu Baerts <matth...@baerts.eu>
+sub   rsa4096 2015-08-31 [E]
+      5B1A4BFBA06327FAC85B89272F9C14FD7EDF0E65
+sub   ed25519 2021-08-26 [S]
+      1B86596F99E77A0D744A4E387C22F0C2F3470A97
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+=CUrO
+-----END PGP PUBLIC KEY BLOCK-----
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/b4/__init__.py new/b4-0.12.0/b4/__init__.py
--- old/b4-0.11.2/b4/__init__.py        2023-01-05 17:30:41.000000000 +0100
+++ new/b4-0.12.0/b4/__init__.py        2023-01-20 16:31:00.000000000 +0100
@@ -12,11 +12,13 @@
 import email.policy
 import email.header
 import email.generator
+import email.quoprimime
 import tempfile
 import pathlib
 import argparse
 import smtplib
 import shlex
+import textwrap
 
 import urllib.parse
 import datetime
@@ -31,13 +33,17 @@
 
 from pathlib import Path
 from contextlib import contextmanager
-from typing import Optional, Tuple, Set, List, BinaryIO, Union, Sequence
+from typing import Optional, Tuple, Set, List, BinaryIO, Union, Sequence, 
Literal
 
 from email import charset
 charset.add_charset('utf-8', None)
 # Policy we use for saving mail locally
 emlpolicy = email.policy.EmailPolicy(utf8=True, cte_type='8bit', 
max_line_length=None)
 
+# Presence of these characters requires quoting of the name in the header
+# adapted from email._parseaddr
+qspecials = re.compile(r'[()<>@,:;.\"\[\]]')
+
 try:
     import dkim
     can_dkim = True
@@ -53,7 +59,7 @@
 # global setting allowing us to turn off networking
 can_network = True
 
-__VERSION__ = '0.11.2'
+__VERSION__ = '0.12.0'
 PW_REST_API_VERSION = '1.2'
 
 
@@ -769,7 +775,7 @@
 
             if stalecache:
                 logger.debug('Stale cache for [v%s] %s', self.revision, 
self.subject)
-                save_cache(None, msgid, suffix='fakeam')
+                clear_cache(msgid, suffix='fakeam')
 
         logger.info('Preparing fake-am for v%s: %s', self.revision, 
self.subject)
         with git_temp_worktree(gitdir):
@@ -848,8 +854,8 @@
     def save_cover(self, outfile):
         # noinspection PyUnresolvedReferences
         cover_msg = self.patches[0].get_am_message(add_trailers=False)
-        with open(outfile, 'w') as fh:
-            fh.write(cover_msg.as_string(policy=emlpolicy))
+        with open(outfile, 'wb') as fh:
+            fh.write(LoreMessage.get_msg_as_bytes(cover_msg, headers='decode'))
         logger.critical('Cover: %s', outfile)
 
 
@@ -1395,21 +1401,110 @@
         if hdrval is None:
             return ''
 
-        decoded = ''
-        for hstr, hcs in email.header.decode_header(hdrval):
-            if hcs is None:
-                hcs = 'utf-8'
-            try:
-                decoded += hstr.decode(hcs, errors='replace')
-            except LookupError:
-                # Try as utf-u
-                decoded += hstr.decode('utf-8', errors='replace')
-            except (UnicodeDecodeError, AttributeError):
-                decoded += hstr
+        if hdrval.find('=?') >= 0:
+            # Do we have any email addresses in there?
+            if re.search(r'<\S+@\S+>', hdrval, flags=re.I | re.M):
+                newaddrs = list()
+                for addr in email.utils.getaddresses([hdrval]):
+                    if addr[0].find('=?') >= 0:
+                        # Nothing wrong with nested calls, right?
+                        addr = (LoreMessage.clean_header(addr[0]), addr[1])
+                    # Work around 
https://github.com/python/cpython/issues/100900
+                    if re.search(r'[^\w\s]', addr[0]):
+                        newaddrs.append(f'"{addr[0]}" <{addr[1]}>')
+                    else:
+                        newaddrs.append(email.utils.formataddr(addr))
+                return ', '.join(newaddrs)
+
+            decoded = ''
+            for hstr, hcs in email.header.decode_header(hdrval):
+                if hcs is None:
+                    hcs = 'utf-8'
+                try:
+                    decoded += hstr.decode(hcs, errors='replace')
+                except LookupError:
+                    # Try as utf-8
+                    decoded += hstr.decode('utf-8', errors='replace')
+                except (UnicodeDecodeError, AttributeError):
+                    decoded += hstr
+        else:
+            decoded = hdrval
+
         new_hdrval = re.sub(r'\n?\s+', ' ', decoded)
         return new_hdrval.strip()
 
     @staticmethod
+    def wrap_header(hdr, width: int = 75, nl: str = '\n',
+                    transform: Literal['encode', 'decode', 'preserve'] = 
'preserve') -> bytes:
+        hname, hval = hdr
+        if hname.lower() in ('to', 'cc', 'from', 'x-original-from'):
+            _parts = [f'{hname}: ',]
+            first = True
+            for addr in email.utils.getaddresses([hval]):
+                if transform == 'encode' and not addr[0].isascii():
+                    addr = (email.quoprimime.header_encode(addr[0].encode(), 
charset='utf-8'), addr[1])
+                    qp = format_addrs([addr], clean=False)
+                elif transform == 'decode':
+                    qp = format_addrs([addr], clean=True)
+                else:
+                    qp = format_addrs([addr], clean=False)
+                # See if there is enough room on the existing line
+                if first:
+                    _parts[-1] += qp
+                    first = False
+                    continue
+                if len(_parts[-1] + ', ' + qp) > width:
+                    _parts[-1] += ', '
+                    _parts.append(qp)
+                    continue
+                _parts[-1] += ', ' + qp
+        else:
+            if transform == 'decode' and hval.find('?=') >= 0:
+                hdata = f'{hname}: ' + LoreMessage.clean_header(hval)
+            else:
+                hdata = f'{hname}: {hval}'
+            if transform != 'encode' or hval.isascii():
+                if len(hdata) <= width:
+                    return hdata.encode()
+                # Use simple textwrap, with a small trick that ensures that 
long non-breakable
+                # strings don't show up on the next line from the bare header
+                hdata = hdata.replace(': ', ':_', 1)
+                wrapped = textwrap.wrap(hdata, break_long_words=False, 
break_on_hyphens=False,
+                                        subsequent_indent=' ', width=width)
+                return nl.join(wrapped).replace(':_', ': ', 1).encode()
+
+            qp = f'{hname}: ' + email.quoprimime.header_encode(hval.encode(), 
charset='utf-8')
+            # is it longer than width?
+            if len(qp) <= width:
+                return qp.encode()
+
+            _parts = list()
+            while len(qp) > width:
+                wrapat = width - 2
+                if len(_parts):
+                    # Also allow for the ' ' at the front on continuation lines
+                    wrapat -= 1
+                # Make sure we don't break on a =XX escape sequence
+                while '=' in qp[wrapat-2:wrapat]:
+                    wrapat -= 1
+                _parts.append(qp[:wrapat] + '?=')
+                qp = ('=?utf-8?q?' + qp[wrapat:])
+            _parts.append(qp)
+        return f'{nl} '.join(_parts).encode()
+
+    @staticmethod
+    def get_msg_as_bytes(msg: email.message.Message, nl: str ='\n',
+                         headers: Literal['encode', 'decode', 'preserve'] = 
'preserve') -> bytes:
+        bdata = b''
+        for hname, hval in msg.items():
+            bdata += LoreMessage.wrap_header((hname, str(hval)), nl=nl, 
transform=headers) + nl.encode()
+        bdata += nl.encode()
+        payload = msg.get_payload(decode=True)
+        for bline in payload.split(b'\n'):
+            bdata += re.sub(rb'[\r\n]*$', b'', bline) + nl.encode()
+        return bdata
+
+    @staticmethod
     def get_parts_from_header(hstr: str) -> dict:
         hstr = re.sub(r'\s*', '', hstr)
         hdata = dict()
@@ -2415,7 +2510,7 @@
     return cachedir
 
 
-def get_cache_file(identifier, suffix=None):
+def get_cache_file(identifier: str, suffix: Optional[str] = None):
     cachedir = get_cache_dir()
     cachefile = hashlib.sha1(identifier.encode()).hexdigest()
     if suffix:
@@ -2423,7 +2518,7 @@
     return os.path.join(cachedir, cachefile)
 
 
-def get_cache(identifier, suffix=None):
+def get_cache(identifier: str, suffix: Optional[str] = None) -> Optional[str]:
     fullpath = get_cache_file(identifier, suffix=suffix)
     try:
         with open(fullpath) as fh:
@@ -2434,15 +2529,15 @@
     return None
 
 
-def save_cache(contents, identifier, suffix=None, mode='w'):
+def clear_cache(identifier: str, suffix: Optional[str] = None) -> None:
+    fullpath = get_cache_file(identifier, suffix=suffix)
+    if os.path.exists(fullpath):
+        os.unlink(fullpath)
+        logger.debug('Removed cache %s for %s', fullpath, identifier)
+
+
+def save_cache(contents: str, identifier: str, suffix: Optional[str] = None, 
mode: str = 'w') -> None:
     fullpath = get_cache_file(identifier, suffix=suffix)
-    if not contents:
-        # noinspection PyBroadException
-        try:
-            os.unlink(fullpath)
-            logger.debug('Removed cache %s for %s', fullpath, identifier)
-        except:
-            pass
     try:
         with open(fullpath, mode) as fh:
             fh.write(contents)
@@ -2815,7 +2910,7 @@
         msg.replace_header('From', setfrom)
 
         if seriests:
-            patchts = seriests + counter
+            patchts = seriests + counter + 1
             origdate = msg.get('Date')
             if origdate:
                 msg.replace_header('Date', email.utils.formatdate(patchts, 
localtime=True))
@@ -2870,13 +2965,18 @@
 def format_addrs(pairs, clean=True):
     addrs = list()
     for pair in pairs:
-        pair = list(pair)
         if pair[0] == pair[1]:
-            pair[0] = ''
+            addrs.append(pair[1])
+            continue
         if clean:
             # Remove any quoted-printable header junk from the name
-            pair[0] = LoreMessage.clean_header(pair[0])
-        addrs.append(email.utils.formataddr(pair))  # noqa
+            pair = (LoreMessage.clean_header(pair[0]), pair[1])
+        # Work around https://github.com/python/cpython/issues/100900
+        if not pair[0].startswith('=?') and not pair[0].startswith('"') and 
qspecials.search(pair[0]):
+            quoted = email.utils.quote(pair[0])
+            addrs.append(f'"{quoted}" <{pair[1]}>')
+            continue
+        addrs.append(email.utils.formataddr(pair))
     return ', '.join(addrs)
 
 
@@ -2966,17 +3066,22 @@
     return uids
 
 
-def save_git_am_mbox(msgs: list, dest: BinaryIO):
+def save_git_am_mbox(msgs: list[email.message.Message], dest: BinaryIO):
     # Git-am has its own understanding of what "mbox" format is that differs 
from Python's
     # mboxo implementation. Specifically, it never escapes the ">From " lines 
found in bodies
     # unless invoked with --patch-format=mboxrd (this is wrong, because ">From 
" escapes are also
     # required in the original mbox "mboxo" format).
     # So, save in the format that git-am expects
-    gen = email.generator.BytesGenerator(dest, policy=emlpolicy)
     for msg in msgs:
-        msg.set_unixfrom('From git@z Thu Jan  1 00:00:00 1970')
-        gen.flatten(msg, unixfrom=True)
-        gen.write('\n')
+        dest.write(b'From git@z Thu Jan  1 00:00:00 1970\n')
+        dest.write(LoreMessage.get_msg_as_bytes(msg, headers='decode'))
+
+
+def save_mboxrd_mbox(msgs: list[email.message.Message], dest: BinaryIO, 
mangle_from: bool = False):
+    gen = email.generator.BytesGenerator(dest, mangle_from_=mangle_from, 
policy=emlpolicy)
+    for msg in msgs:
+        dest.write(b'From mboxrd@z Thu Jan  1 00:00:00 1970\n')
+        gen.flatten(msg)
 
 
 def save_maildir(msgs: list, dest):
@@ -2991,7 +3096,7 @@
         lsubj = LoreSubject(msg.get('subject', ''))
         slug = '%04d_%s' % (lsubj.counter, re.sub(r'\W+', '_', 
lsubj.subject).strip('_').lower())
         with open(os.path.join(d_tmp, f'{slug}.eml'), 'wb') as mfh:
-            mfh.write(msg.as_bytes(policy=emlpolicy))
+            mfh.write(LoreMessage.get_msg_as_bytes(msg, headers='decode'))
         os.rename(os.path.join(d_tmp, f'{slug}.eml'), os.path.join(d_new, 
f'{slug}.eml'))
 
 
@@ -3211,8 +3316,8 @@
 def send_mail(smtp: Union[smtplib.SMTP, smtplib.SMTP_SSL, None], msgs: 
Sequence[email.message.Message],
               fromaddr: Optional[str], destaddrs: Optional[Union[set, list]] = 
None,
               patatt_sign: bool = False, dryrun: bool = False,
-              maxheaderlen: Optional[int] = None, output_dir: Optional[str] = 
None,
-              web_endpoint: Optional[str] = None, reflect: bool = False) -> 
Optional[int]:
+              output_dir: Optional[str] = None, web_endpoint: Optional[str] = 
None,
+              reflect: bool = False) -> Optional[int]:
 
     tosend = list()
     if output_dir is not None:
@@ -3222,34 +3327,13 @@
         if not msg.get('X-Mailer'):
             msg.add_header('X-Mailer', f'b4 {__VERSION__}')
         msg.set_charset('utf-8')
-        if maxheaderlen is None:
-            if dryrun:
-                # Make it fit the terminal window, but no wider than 120 minus 
visual padding
-                ts = shutil.get_terminal_size((120, 20))
-                maxheaderlen = ts.columns - 8
-                if maxheaderlen > 112:
-                    maxheaderlen = 112
-            else:
-                # Use a sane-ish default (we don't need to stick to 80, but
-                # we need to make sure it's shorter than 255)
-                maxheaderlen = 120
-
-        if dryrun and not output_dir:
-            # Use 8bit-clean policy if we're dumping things to screen
-            emldata = msg.as_string(policy=emlpolicy, 
maxheaderlen=maxheaderlen)
-            bdata = emldata.encode()
-        else:
-            # Use SMTP policy if we're actually going to send things out
-            msg = sevenbitify_headers(msg)
-            if dryrun:
-                # Use HTTP policy, to avoid header wrapping
-                policy = email.policy.HTTP.clone(linesep='\n')
-            elif web_endpoint:
-                # Use SMTP policy with LF endings
-                policy = email.policy.SMTP.clone(linesep='\n')
-            else:
-                policy = email.policy.SMTP
-            bdata = msg.as_bytes(policy=policy)
+
+        if dryrun or web_endpoint:
+            nl = '\n'
+        else:
+            nl = '\r\n'
+
+        bdata = LoreMessage.get_msg_as_bytes(msg, nl=nl, headers='encode')
 
         subject = msg.get('Subject', '')
         ls = LoreSubject(subject)
@@ -3479,52 +3563,6 @@
 
     return msgid, msgs
 
-
-# When qp-encoding a header, encode just the 8bit content,
-# not the entire header.
-def qp_smallest(srcstr: str) -> str:
-    import email.quoprimime
-    qpstr = ''
-    toqp = ''
-    chunks = re.split(r'(\S+)', srcstr)
-    for pos, chunk in enumerate(chunks):
-        if not len(toqp):
-            if chunk.isascii():
-                qpstr += chunk
-                continue
-            toqp += chunk
-            continue
-        # is this chunk whitespace?
-        if not len(chunk.strip()):
-            # are all the remaining words after this one ascii?
-            if pos+1 >= len(chunks) or all(_chunk.isascii() for _chunk in 
chunks[pos+1:]):
-                qpstr += email.quoprimime.header_encode(toqp.encode(), 
charset='utf-8') + chunk
-                toqp = ''
-                continue
-        toqp += chunk
-    return qpstr
-
-
-def sevenbitify_headers(msg: email.message.Message) -> email.message.Message:
-    import email.quoprimime
-    for pos, hdr in enumerate(msg._headers):  # noqa
-        if hdr[1].isascii():
-            continue
-        # Do we have an email address in there?
-        if re.search(r'<\S+@\S+>', hdr[1], flags=re.I | re.M):
-            newaddrs = list()
-            for addr in email.utils.getaddresses([hdr[1]]):
-                if addr[0].isascii():
-                    newaddrs.append(addr)
-                    continue
-                newaddrs.append((qp_smallest(addr[0]), addr[1]))
-            newval = format_addrs(newaddrs, clean=False)
-        else:
-            newval = qp_smallest(hdr[1])
-        msg._headers[pos] = (hdr[0], newval)  # noqa
-    return msg
-
-
 def git_revparse_obj(gitobj: str) -> str:
     ecode, out = git_run_command(None, ['rev-parse', gitobj])
     if ecode > 0:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/b4/command.py new/b4-0.12.0/b4/command.py
--- old/b4-0.11.2/b4/command.py 2023-01-05 17:30:41.000000000 +0100
+++ new/b4-0.12.0/b4/command.py 2023-01-20 16:31:00.000000000 +0100
@@ -138,6 +138,8 @@
     cmd_mbox_common_opts(sp_mbox)
     sp_mbox.add_argument('-f', '--filter-dupes', dest='filterdupes', 
action='store_true', default=False,
                          help='When adding messages to existing maildir, 
filter out duplicates')
+    sp_mbox.add_argument('-r', '--refetch', dest='refetch', metavar='MBOX', 
default=False,
+                         help='Refetch all messages in specified mbox with 
their original headers')
     sp_mbox.set_defaults(func=cmd_mbox)
 
     # b4 am
@@ -255,23 +257,24 @@
 
     # b4 prep
     sp_prep = subparsers.add_parser('prep', help='Work on patch series to 
submit for mailing list review')
-    spp_g = sp_prep.add_mutually_exclusive_group()
-    spp_g.add_argument('-c', '--auto-to-cc', action='store_true', 
default=False,
+    sp_prep.add_argument('-c', '--auto-to-cc', action='store_true', 
default=False,
                        help='Automatically populate cover letter trailers with 
To and Cc addresses')
+    sp_prep.add_argument('--force-revision', metavar='N', type=int,
+                       help='Force revision to be this number instead')
+    sp_prep.add_argument('--set-prefixes', metavar='PREFIX', nargs='+',
+                       help='Extra prefixes to add to [PATCH] (e.g.: RFC 
mydrv)')
+
+    spp_g = sp_prep.add_mutually_exclusive_group()
     spp_g.add_argument('-p', '--format-patch', metavar='OUTPUT_DIR',
                        help='Output prep-tracked commits as patches')
     spp_g.add_argument('--edit-cover', action='store_true', default=False,
                        help='Edit the cover letter in your defined $EDITOR (or 
core.editor)')
     spp_g.add_argument('--show-revision', action='store_true', default=False,
                        help='Show current series revision number')
-    spp_g.add_argument('--force-revision', metavar='N', type=int,
-                       help='Force revision to be this number instead')
     spp_g.add_argument('--compare-to', metavar='vN',
                        help='Display a range-diff to previously sent revision 
N')
     spp_g.add_argument('--manual-reroll', dest='reroll', default=None, 
metavar='COVER_MSGID',
                        help='Mark current revision as sent and reroll 
(requires cover letter msgid)')
-    spp_g.add_argument('--set-prefixes', metavar='PREFIX', nargs='+',
-                       help='Extra prefixes to add to [PATCH] (e.g.: RFC 
mydrv)')
     spp_g.add_argument('--show-info', action='store_true', default=False,
                        help='Show current series info in a column-parseable 
format')
 
@@ -310,8 +313,6 @@
                          help='Send everything to yourself instead of the 
actual recipients')
     sp_send.add_argument('--no-trailer-to-cc', action='store_true', 
default=False,
                          help='Do not add any addresses found in the cover or 
patch trailers to To: or Cc:')
-    sp_send.add_argument('--hide-cover-to-cc', action='store_true', 
default=False,
-                         help='Hide To: and Cc: entries from the cover letter 
trailers (but still send to them)')
     sp_send.add_argument('--to', nargs='+', help='Addresses to add to the To: 
list')
     sp_send.add_argument('--cc', nargs='+', help='Addresses to add to the Cc: 
list')
     sp_send.add_argument('--not-me-too', action='store_true', default=False,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/b4/ez.py new/b4-0.12.0/b4/ez.py
--- old/b4-0.11.2/b4/ez.py      2023-01-05 17:30:41.000000000 +0100
+++ new/b4-0.12.0/b4/ez.py      2023-01-20 16:31:00.000000000 +0100
@@ -451,21 +451,22 @@
         # create a default cover letter and store it where the strategy 
indicates
         cover = ('EDITME: cover title for %s' % seriesname,
                  '',
-                 '# Lines starting with # will be removed from the cover 
letter. You can use',
-                 '# them to add notes or reminders to yourself.',
+                 '# Lines starting with # will be removed from the cover 
letter. You can',
+                 '# use them to add notes or reminders to yourself.',
                  '',
-                 'EDITME: describe the purpose of this series. The information 
you put here',
-                 'will be used by the project maintainer to make a decision 
whether your',
-                 'patches should be reviewed, and in what priority order. 
Please be very',
-                 'detailed and link to any relevant discussions or sites that 
the maintainer',
-                 'can review to better understand your proposed changes. If 
you only have a',
-                 'single patch in your series, the contents of the cover 
letter will be',
-                 'appended to the "under-the-cut" portion of the patch.',
+                 'EDITME: describe the purpose of this series. The information 
you put',
+                 'here will be used by the project maintainer to make a 
decision whether',
+                 'your patches should be reviewed, and in what priority order. 
Please be',
+                 'very detailed and link to any relevant discussions or sites 
that the',
+                 'maintainer can review to better understand your proposed 
changes. If you',
+                 'only have a single patch in your series, the contents of the 
cover',
+                 'letter will be appended to the "under-the-cut" portion of 
the patch.',
                  '',
                  '# You can add trailers to the cover letter. Any email 
addresses found in',
-                 '# these trailers will be added to the addresses 
specified/generated during',
-                 '# the b4 send stage. You can also run "b4 prep --auto-to-cc" 
to auto-populate',
-                 '# the To: and Cc: trailers based on the code being 
modified.',
+                 '# these trailers will be added to the addresses 
specified/generated',
+                 '# during the b4 send stage. You can also run "b4 prep 
--auto-to-cc" to',
+                 '# auto-populate the To: and Cc: trailers based on the code 
being',
+                 '# modified.',
                  '',
                  'Signed-off-by: %s <%s>' % (usercfg.get('name', ''), 
usercfg.get('email', '')),
                  '',
@@ -480,7 +481,7 @@
         if revision is None:
             revision = 1
         prefixes = list()
-        if cmdargs.set_prefixes and len(prefixes[0].strip()):
+        if cmdargs.set_prefixes:
             prefixes = list(cmdargs.set_prefixes)
         else:
             config = b4.get_main_config()
@@ -686,17 +687,21 @@
     corecfg = b4.get_config_from_git(r'core\..*', {'editor': 
os.environ.get('EDITOR', 'vi')})
     editor = corecfg.get('editor')
     logger.debug('editor=%s', editor)
-    # We give it a suffix .rst in hopes that editors autoload 
restructured-text rules
-    with tempfile.NamedTemporaryFile(suffix='.rst') as temp_cover:
-        temp_cover.write(cover.encode())
-        temp_cover.seek(0)
+    # Use COMMIT_EDITMSG name in hopes that editors autoload git commit rules
+    with tempfile.TemporaryDirectory(prefix='b4-') as temp_dir:
+        temp_fpath = os.path.join(temp_dir, 'COMMIT_EDITMSG')
+        with open(temp_fpath, 'xb') as temp_cover:
+            temp_cover.write(cover.encode())
+
         sp = shlex.shlex(editor, posix=True)
         sp.whitespace_split = True
-        cmdargs = list(sp) + [temp_cover.name]
+        cmdargs = list(sp) + [temp_fpath]
         logger.debug('Running %s' % ' '.join(cmdargs))
         sp = subprocess.Popen(cmdargs)
         sp.wait()
-        new_cover = temp_cover.read().decode(errors='replace').strip()
+
+        with open(temp_fpath, 'rb') as temp_cover:
+            new_cover = temp_cover.read().decode(errors='replace').strip()
 
     if new_cover == cover:
         logger.info('Cover letter unchanged.')
@@ -1001,7 +1006,7 @@
     return msgid_tpt
 
 
-def get_cover_dests(cbody: str, hide: bool = True) -> Tuple[List, List, str]:
+def get_cover_dests(cbody: str) -> Tuple[List, List, str]:
     htrs, cmsg, mtrs, basement, sig = b4.LoreMessage.get_body_parts(cbody)
     tos = list()
     ccs = list()
@@ -1012,8 +1017,7 @@
         elif mtr.lname == 'cc':
             ccs.append(mtr.addr)
             mtrs.remove(mtr)
-    if hide:
-        cbody = b4.LoreMessage.rebuild_message(htrs, cmsg, mtrs, basement, sig)
+    cbody = b4.LoreMessage.rebuild_message(htrs, cmsg, mtrs, basement, sig)
     return tos, ccs, cbody
 
 
@@ -1113,8 +1117,7 @@
     return usercfg.get('name'), usercfg.get('email')
 
 
-def get_prep_branch_as_patches(movefrom: bool = True, thread: bool = True,
-                               addtracking: bool = True, hide_cover_to_cc: 
bool = False
+def get_prep_branch_as_patches(movefrom: bool = True, thread: bool = True, 
addtracking: bool = True
                                ) -> Tuple[List, List, str, List[Tuple[str, 
email.message.Message]]]:
     cover, tracking = load_cover(strip_comments=True)
 
@@ -1176,10 +1179,10 @@
     ztracking = gzip.compress(bytes(json.dumps(tracking), 'utf-8'))
     b64tracking = base64.b64encode(ztracking).decode()
     # A little trick for pretty wrapping
-    wrapped = textwrap.wrap('X-B4-Tracking: v=1; b=' + b64tracking, width=75)
-    thdata = ' '.join(wrapped).replace('X-B4-Tracking: ', '')
+    wrapped = textwrap.wrap('X-B4-Tracking: v=1; b=' + b64tracking, 
subsequent_indent=' ', width=75)
+    thdata = ''.join(wrapped).replace('X-B4-Tracking: ', '')
 
-    alltos, allccs, cbody = get_cover_dests(cover_letter, 
hide=hide_cover_to_cc)
+    alltos, allccs, cbody = get_cover_dests(cover_letter)
     if len(patches) == 1:
         mixin_cover(cbody, patches)
     else:
@@ -1192,8 +1195,7 @@
     return alltos, allccs, tag_msg, patches
 
 
-def get_sent_tag_as_patches(tagname: str, revision: int, hide_cover_to_cc: 
bool = False
-                            ) -> Tuple[List, List, List[Tuple[str, 
email.message.Message]]]:
+def get_sent_tag_as_patches(tagname: str, revision: int) -> Tuple[List, List, 
List[Tuple[str, email.message.Message]]]:
     cover, base_commit, change_id = get_base_changeid_from_tag(tagname)
 
     csubject, cbody = get_cover_subject_body(cover)
@@ -1210,7 +1212,7 @@
                                       seriests=seriests,
                                       mailfrom=mailfrom)
 
-    alltos, allccs, cbody = get_cover_dests(cbody, hide=hide_cover_to_cc)
+    alltos, allccs, cbody = get_cover_dests(cbody)
     if len(patches) == 1:
         mixin_cover(cbody, patches)
     else:
@@ -1251,8 +1253,6 @@
     mybranch = b4.git_get_current_branch()
 
     config = b4.get_main_config()
-    if not cmdargs.hide_cover_to_cc and config.get('send-hide-cover-to-cc', 
'').lower() in {'yes', 'true', '1'}:
-        cmdargs.hide_cover_to_cc = True
 
     tag_msg = None
     cl_msgid = None
@@ -1264,8 +1264,7 @@
             sys.exit(1)
 
         try:
-            todests, ccdests, patches = get_sent_tag_as_patches(tagname, 
revision=revision,
-                                                                
hide_cover_to_cc=cmdargs.hide_cover_to_cc)
+            todests, ccdests, patches = get_sent_tag_as_patches(tagname, 
revision=revision)
         except RuntimeError as ex:
             logger.critical('CRITICAL: Failed to convert tag to patches: %s', 
ex)
             sys.exit(1)
@@ -1282,9 +1281,14 @@
             logger.info('---')
             sys.exit(1)
 
+        status = b4.git_get_repo_status()
+        if len(status):
+            logger.critical('CRITICAL: Repository contains uncommitted 
changes.')
+            logger.critical('          Stash or commit them first.')
+            sys.exit(1)
+
         try:
-            todests, ccdests, tag_msg, patches = get_prep_branch_as_patches(
-                hide_cover_to_cc=cmdargs.hide_cover_to_cc)
+            todests, ccdests, tag_msg, patches = get_prep_branch_as_patches()
         except RuntimeError as ex:
             logger.critical('CRITICAL: Failed to convert range to patches: 
%s', ex)
             sys.exit(1)
@@ -1314,13 +1318,11 @@
                     continue
                 if btr.addr[1] in seen:
                     continue
-                if commit and btr.lname == 'cc':
-                    # CC's in individual patches don't get added to global 
Cc's,
-                    # we use the pcss dict to track them.
+                if commit:
                     if commit not in pccs:
                         pccs[commit] = list()
-                    pccs[commit].append(btr.addr)
-                    # Doesn't get added to seen, in case a later patch puts it 
into global
+                    if btr.addr not in pccs[commit]:
+                        pccs[commit].append(btr.addr)
                     continue
                 seen.add(btr.addr[1])
                 if btr.lname == 'to':
@@ -1381,15 +1383,20 @@
         pathlib.Path(cmdargs.output_dir).mkdir(parents=True, exist_ok=True)
 
     sconfig = b4.get_sendemail_config()
-    endpoint = config.get('send-endpoint-web', '')
-    if not re.search(r'^https?://', endpoint):
-        endpoint = None
-    if not endpoint and not sconfig.get('smtpserver'):
-        # Use the default endpoint if we are in the kernel repo
-        topdir = b4.git_get_toplevel()
-        if os.path.exists(os.path.join(topdir, 'Kconfig')):
-            logger.debug('No sendemail configs found, will use the default web 
endpoint')
-            endpoint = DEFAULT_ENDPOINT
+    # If we have an smtp server defined, always use that instead of the 
endpoint
+    # we may make this configurable in the future, but this almost always 
makes sense
+    endpoint = None
+    if not sconfig.get('smtpserver'):
+        endpoint = config.get('send-endpoint-web', '')
+        if not re.search(r'^https?://', endpoint):
+            logger.debug('Endpoint does not start with https, ignoring: %s', 
endpoint)
+            endpoint = None
+        if not endpoint:
+            # Use the default endpoint if we are in the kernel repo
+            topdir = b4.git_get_toplevel()
+            if os.path.exists(os.path.join(topdir, 'Kconfig')):
+                logger.debug('No sendemail configs found, will use the default 
web endpoint')
+                endpoint = DEFAULT_ENDPOINT
 
     # Give the user the last opportunity to bail out
     if not cmdargs.dryrun:
@@ -1821,32 +1828,19 @@
             logger.debug('added %s to seen', ltr.addr[1])
             extras.append(ltr)
 
-    tos, ccs, tag_msg, patches = get_prep_branch_as_patches()
+    try:
+        tos, ccs, tag_msg, patches = get_prep_branch_as_patches()
+    except RuntimeError:
+        logger.info('No commits in branch')
+        return
+
     logger.info('Collecting To/Cc addresses')
     # Go through the messages to make to/cc headers
     for commit, msg in patches:
-        if not msg:
-            continue
-        payload = msg.get_payload(decode=True).decode()
-        parts = b4.LoreMessage.get_body_parts(payload)
-        for ltr in parts[2]:
-            if ltr.lname == 'cc':
-                # We treat Cc: in individual patches differently from all 
other trailers:
-                # they only receive individual patches, not the entire series.
-                continue
-            if not ltr.addr:
-                continue
-            if ltr.addr[1] in seen:
-                continue
-            seen.add(ltr.addr[1])
-            logger.debug('added %s to seen', ltr.addr[1])
-            # Make it a Cc: trailer
-            ltr.name = 'Cc'
-            extras.append(ltr)
-
-        if not commit:
+        if not msg or not commit:
             continue
 
+        logger.debug('Collecting from: %s', msg.get('subject'))
         msgbytes = msg.as_bytes()
         for tname, pairs in (('To', get_addresses_from_cmd(tocmd, msgbytes)),
                              ('Cc', get_addresses_from_cmd(cccmd, msgbytes))):
@@ -1854,6 +1848,7 @@
                 if pair[1] not in seen:
                     seen.add(pair[1])
                     ltr = b4.LoreTrailer(name=tname, 
value=b4.format_addrs([pair]))
+                    logger.debug('  => %s', ltr.as_string())
                     extras.append(ltr)
 
     if not extras:
@@ -1895,12 +1890,6 @@
         logger.critical('          Stash or commit them first.')
         sys.exit(1)
 
-    if cmdargs.edit_cover:
-        return edit_cover()
-
-    if cmdargs.auto_to_cc:
-        return auto_to_cc()
-
     if cmdargs.reroll:
         msgid = cmdargs.reroll
         msgs = b4.get_pi_thread_by_msgid(msgid, onlymsgids={msgid}, 
nocache=True)
@@ -1927,23 +1916,30 @@
     if cmdargs.show_info:
         return show_info()
 
-    if cmdargs.force_revision:
-        return force_revision(cmdargs.force_revision)
-
     if cmdargs.format_patch:
         return format_patch(cmdargs.format_patch)
 
     if cmdargs.compare_to:
         return compare(cmdargs.compare_to)
 
+    if cmdargs.enroll_base or cmdargs.new_series_name:
+        if is_prep_branch():
+            logger.critical('CRITICAL: This appears to already be a b4-prep 
managed branch.')
+            sys.exit(1)
+
+        start_new_series(cmdargs)
+
+    if cmdargs.force_revision:
+        force_revision(cmdargs.force_revision)
+
     if cmdargs.set_prefixes:
-        return set_prefixes(cmdargs.set_prefixes)
+        set_prefixes(cmdargs.set_prefixes)
 
-    if is_prep_branch():
-        logger.critical('CRITICAL: This appears to already be a b4-prep 
managed branch.')
-        sys.exit(1)
+    if cmdargs.auto_to_cc:
+        auto_to_cc()
 
-    return start_new_series(cmdargs)
+    if cmdargs.edit_cover:
+        return edit_cover()
 
 
 def cmd_trailers(cmdargs: argparse.Namespace) -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/b4/mbox.py new/b4-0.12.0/b4/mbox.py
--- old/b4-0.11.2/b4/mbox.py    2023-01-05 17:30:41.000000000 +0100
+++ new/b4-0.12.0/b4/mbox.py    2023-01-20 16:31:00.000000000 +0100
@@ -638,6 +638,37 @@
     return msgs
 
 
+def is_maildir(dest: str) -> bool:
+    if (os.path.isdir(os.path.join(dest, 'new'))
+            and os.path.isdir(os.path.join(dest, 'cur'))
+            and os.path.isdir(os.path.join(dest, 'tmp'))):
+        return True
+    return False
+
+
+def refetch(dest: str) -> None:
+    if is_maildir(dest):
+        mbox = mailbox.Maildir(dest)
+    else:
+        mbox = mailbox.mbox(dest)
+
+    by_msgid = dict()
+    for key, msg in mbox.items():
+        msgid = b4.LoreMessage.get_clean_msgid(msg)
+        if msgid not in by_msgid:
+            amsgs = b4.get_pi_thread_by_msgid(msgid, nocache=True)
+            for amsg in amsgs:
+                amsgid = b4.LoreMessage.get_clean_msgid(amsg)
+                if amsgid not in by_msgid:
+                    by_msgid[amsgid] = amsg
+        if msgid in by_msgid:
+            mbox.update(((key, by_msgid[msgid]),))
+            logger.info('Refetched: %s', msg.get('Subject'))
+        else:
+            logger.warn('WARNING: Message-id not known: %s', msgid)
+    mbox.close()
+
+
 def main(cmdargs: argparse.Namespace) -> None:
     if cmdargs.subcmd == 'shazam':
         # We force some settings
@@ -657,6 +688,9 @@
         # Force nocache mode
         cmdargs.nocache = True
 
+    if cmdargs.subcmd == 'mbox' and cmdargs.refetch:
+        return refetch(cmdargs.refetch)
+
     try:
         msgid, msgs = b4.retrieve_messages(cmdargs)
     except LookupError as ex:
@@ -676,13 +710,11 @@
     logger.info('%s messages in the thread', len(msgs))
     if cmdargs.outdir == '-':
         logger.info('---')
-        b4.save_git_am_mbox(msgs, sys.stdout.buffer)
+        b4.save_mboxrd_mbox(msgs, sys.stdout.buffer, mangle_from=False)
         return
 
     # Check if outdir is a maildir
-    if (os.path.isdir(os.path.join(cmdargs.outdir, 'new'))
-            and os.path.isdir(os.path.join(cmdargs.outdir, 'cur'))
-            and os.path.isdir(os.path.join(cmdargs.outdir, 'tmp'))):
+    if is_maildir(cmdargs.outdir):
         mdr = mailbox.Maildir(cmdargs.outdir)
         have_msgids = set()
         added = 0
@@ -721,6 +753,6 @@
         return
 
     with open(savename, 'wb') as fh:
-        b4.save_git_am_mbox(msgs, fh)
+        b4.save_mboxrd_mbox(msgs, fh, mangle_from=True)
 
     logger.info('Saved %s', savename)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/docs/config.rst 
new/b4-0.12.0/docs/config.rst
--- old/b4-0.11.2/docs/config.rst       2023-01-05 17:30:41.000000000 +0100
+++ new/b4-0.12.0/docs/config.rst       2023-01-20 16:31:00.000000000 +0100
@@ -339,12 +339,6 @@
 
   Default: ``no``
 
-``b4.send-hide-cover-to-cc`` (v0.10+)
-  Always hide To: and Cc: trailers from the cover letter, just include
-  them into the corresponding message recipient headers.
-
-  Default: ``no``
-
 ``b4.send-auto-to-cmd`` (v0.10+)
   Command to use to generate the list of To: recipients. Has no effect
   if the specified script is not found in the repository.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/docs/contributor/send.rst 
new/b4-0.12.0/docs/contributor/send.rst
--- old/b4-0.11.2/docs/contributor/send.rst     2023-01-05 17:30:41.000000000 
+0100
+++ new/b4-0.12.0/docs/contributor/send.rst     2023-01-20 16:31:00.000000000 
+0100
@@ -220,14 +220,6 @@
   or Cc:. This is usually handy for testing purposes, in case you want
   to send a set of patches to a test address (also see ``--reflect``).
 
-``--hide-cover-to-cc``
-  It is common for the ``To:`` and ``Cc:`` sections in cover letters to
-  be pretty large on large patch sets. Passing this flag will remove
-  these trailers from the cover letter, but still add the addresses to
-  the corresponding To: and Cc: headers. This can be made permanent in
-  the configuration file using the ``b4.send-hide-cover-to-cc`` option
-  (see :ref:`contributor_settings`).
-
 ``--to``
   Add any more email addresses to include into the To: header here
   (comma-separated). Can be set in the configuration file using the
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/docs/maintainer/mbox.rst 
new/b4-0.12.0/docs/maintainer/mbox.rst
--- old/b4-0.11.2/docs/maintainer/mbox.rst      2023-01-05 17:30:41.000000000 
+0100
+++ new/b4-0.12.0/docs/maintainer/mbox.rst      2023-01-20 16:31:00.000000000 
+0100
@@ -79,6 +79,14 @@
   that aren't already present. Note, that this uses simple message-id
   matching and no other checks for correctness are performed.
 
+``-r MBOX, --refetch MBOX`` **(v0.12+)**
+  This allows you to refetch all messages in the provided mailbox from
+  the upstream public-inbox server. For example, this is useful when you
+  have a .mbx file prepared by ``b4 am`` and you want to send a
+  response to one of the patches. Performing a refetch will restore the
+  original message headers that may have been dropped or modified by
+  ``b4 am``.
+
 Using with mutt
 ---------------
 If you are a mutt or neomutt user and your mail is stored locally, you
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/man/b4.5 new/b4-0.12.0/man/b4.5
--- old/b4-0.11.2/man/b4.5      2023-01-05 17:30:41.000000000 +0100
+++ new/b4-0.12.0/man/b4.5      2023-01-20 16:31:00.000000000 +0100
@@ -27,7 +27,7 @@
 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
 ..
-.TH "B4" 5 "2022-12-05" "0.11.0" ""
+.TH "B4" 5 "2023-01-19" "0.12.0" ""
 .SH NAME
 B4 \- Work with code submissions in a public-inbox archive
 .SH SYNOPSIS
@@ -141,6 +141,9 @@
 .TP
 .B  \-f\fP,\fB  \-\-filter\-dupes
 When adding messages to existing maildir, filter out duplicates
+.TP
+.BI \-r \ MBOX\fR,\fB \ \-\-refetch \ MBOX
+Refetch all messages in specified mbox with their original headers
 .UNINDENT
 .UNINDENT
 .sp
@@ -601,7 +604,7 @@
 .INDENT 0.0
 .TP
 .B usage:
-b4 send [\-h] [\-d] [\-o OUTPUT_DIR] [\-\-reflect] [\-\-no\-trailer\-to\-cc] 
[\-\-hide\-cover\-to\-cc] [\-\-to TO [TO ...]] [\-\-cc CC [CC ...]] 
[\-\-not\-me\-too] [\-\-resend RESEND] [\-\-no\-sign] [\-\-web\-auth\-new] 
[\-\-web\-auth\-verify VERIFY_TOKEN]
+b4 send [\-h] [\-d] [\-o OUTPUT_DIR] [\-\-reflect] [\-\-no\-trailer\-to\-cc] 
[\-\-to TO [TO ...]] [\-\-cc CC [CC ...]] [\-\-not\-me\-too] [\-\-resend 
RESEND] [\-\-no\-sign] [\-\-web\-auth\-new] [\-\-web\-auth\-verify VERIFY_TOKEN]
 .TP
 .B options:
 .INDENT 7.0
@@ -620,9 +623,6 @@
 .TP
 .B  \-\-no\-trailer\-to\-cc
 Do not add any addresses found in the cover or patch trailers to To: or Cc:
-.TP
-.B  \-\-hide\-cover\-to\-cc
-Hide To: and Cc: entries from the cover letter trailers (but still send to 
them)
 .UNINDENT
 .INDENT 7.0
 .TP
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/man/b4.5.rst new/b4-0.12.0/man/b4.5.rst
--- old/b4-0.11.2/man/b4.5.rst  2023-01-05 17:30:41.000000000 +0100
+++ new/b4-0.12.0/man/b4.5.rst  2023-01-20 16:31:00.000000000 +0100
@@ -5,10 +5,10 @@
 ----------------------------------------------------
 
 :Author:    mri...@kernel.org
-:Date:      2022-12-05
+:Date:      2023-01-19
 :Copyright: The Linux Foundation and contributors
 :License:   GPLv2+
-:Version:   0.11.0
+:Version:   0.12.0
 :Manual section: 5
 
 SYNOPSIS
@@ -91,7 +91,9 @@
                         Save as maildir (avoids mbox format ambiguities)
   -f, --filter-dupes
                         When adding messages to existing maildir, filter out 
duplicates
-
+  -r MBOX, --refetch MBOX
+                        Refetch all messages in specified mbox with their 
original headers
+                             
 
 *Example*: b4 mbox 20200313231252.64999-1-keesc...@chromium.org
 
@@ -386,7 +388,7 @@
 b4 send
 ~~~~~~~
 usage:
-  b4 send [-h] [-d] [-o OUTPUT_DIR] [--reflect] [--no-trailer-to-cc] 
[--hide-cover-to-cc] [--to TO [TO ...]] [--cc CC [CC ...]] [--not-me-too] 
[--resend RESEND] [--no-sign] [--web-auth-new] [--web-auth-verify VERIFY_TOKEN]
+  b4 send [-h] [-d] [-o OUTPUT_DIR] [--reflect] [--no-trailer-to-cc] [--to TO 
[TO ...]] [--cc CC [CC ...]] [--not-me-too] [--resend RESEND] [--no-sign] 
[--web-auth-new] [--web-auth-verify VERIFY_TOKEN]
 
 options:
   -h, --help            show this help message and exit
@@ -400,9 +402,6 @@
   --no-trailer-to-cc
                         Do not add any addresses found in the cover or patch 
trailers to To: or Cc:
 
-  --hide-cover-to-cc
-                        Hide To: and Cc: entries from the cover letter 
trailers (but still send to them)
-
   --to TO [TO ...]
                         Addresses to add to the To: list
   --cc CC [CC ...]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/tests/conftest.py 
new/b4-0.12.0/tests/conftest.py
--- old/b4-0.11.2/tests/conftest.py     2023-01-05 17:30:41.000000000 +0100
+++ new/b4-0.12.0/tests/conftest.py     2023-01-20 16:31:00.000000000 +0100
@@ -38,7 +38,7 @@
         bfile = os.path.join(sampledir, 'gitdir.bundle')
     assert os.path.exists(bfile)
     dest = os.path.join(tmp_path, 'repo')
-    args = ['clone', bfile, dest]
+    args = ['clone', '--branch', 'master', bfile, dest]
     out, logstr = b4.git_run_command(None, args)
     assert out == 0
     b4.git_set_config(dest, 'user.name', b4.USER_CONFIG['name'])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/b4-0.11.2/tests/test___init__.py 
new/b4-0.12.0/tests/test___init__.py
--- old/b4-0.11.2/tests/test___init__.py        2023-01-05 17:30:41.000000000 
+0100
+++ new/b4-0.12.0/tests/test___init__.py        2023-01-20 16:31:00.000000000 
+0100
@@ -22,8 +22,9 @@
 @pytest.mark.parametrize('source,regex,flags,ismbox', [
     (None, r'^From git@z ', 0, False),
     (None, r'\n\nFrom git@z ', 0, False),
-    ('save-8bit-clean', r'Unicôdé', 0, True),
-    ('save-7bit-clean', r'=\?utf-8\?q\?S=C3=BBbject\?=', 0, True),
+    ('save-7bit-clean', r'From: Unicôdé', 0, True),
+    # mailbox.mbox does not properly handle 8bit-clean headers
+    ('save-8bit-clean', r'From: Unicôdé', 0, False),
 ])
 def test_save_git_am_mbox(sampledir, tmp_path, source, regex, flags, ismbox):
     import re
@@ -113,24 +114,81 @@
         assert ifh.getvalue().decode() == fh.read()
 
 
-@pytest.mark.parametrize('headers,verify', [
-    ({
-        'From': 'Unicode Nâme <unicode-n...@example.com',
-        'To': 'Ascii Name <ascii-n...@example.com>, '
-              'Unicôde Firstname <unicode-firstn...@example.com>, '
-              'Unicode Lâstname <unicode-lastn...@example.com>',
-        'Subject': 'Subject with unicôde that is randomly interspérsed 
thrôughout the wrapped subject',
-     }, 'sevenbitify-1')
+@pytest.mark.parametrize('hval,verify,tr', [
+    ('short-ascii', 'short-ascii', 'encode'),
+    ('short-unicôde', '=?utf-8?q?short-unic=C3=B4de?=', 'encode'),
+    # Long ascii
+    (('Lorem ipsum dolor sit amet consectetur adipiscing elit '
+      'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'),
+     ('Lorem ipsum dolor sit amet consectetur adipiscing elit sed do\n'
+      ' eiusmod tempor incididunt ut labore et dolore magna aliqua'), 
'encode'),
+    # Long unicode
+    (('Lorem îpsum dolor sit amet consectetur adipiscing elît '
+      'sed do eiusmod tempôr incididunt ut labore et dolôre magna aliqua'),
+     ('=?utf-8?q?Lorem_=C3=AEpsum_dolor_sit_amet_consectetur_adipiscin?=\n'
+      ' 
=?utf-8?q?g_el=C3=AEt_sed_do_eiusmod_temp=C3=B4r_incididunt_ut_labore_et?=\n'
+      ' =?utf-8?q?_dol=C3=B4re_magna_aliqua?='), 'encode'),
+    # Exactly 75 long
+    ('Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiu',
+     'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiu', 
'encode'),
+    # Unicode that breaks on escape boundary
+    ('Lorem ipsum dolor sit amet consectetur adipiscin elît',
+     '=?utf-8?q?Lorem_ipsum_dolor_sit_amet_consectetur_adipiscin_el?=\n 
=?utf-8?q?=C3=AEt?=', 'encode'),
+    # Unicode that's just 1 too long
+    ('Lorem ipsum dolor sit amet consectetur adipi elît',
+     '=?utf-8?q?Lorem_ipsum_dolor_sit_amet_consectetur_adipi_el=C3=AE?=\n 
=?utf-8?q?t?=', 'encode'),
+    # A single address
+    ('f...@example.com', 'f...@example.com', 'encode'),
+    # Two addresses
+    ('f...@example.com, b...@example.com', 'f...@example.com, 
b...@example.com', 'encode'),
+    # Mixed addresses
+    ('f...@example.com, Foo Bar <b...@example.com>', 'f...@example.com, Foo 
Bar <b...@example.com>', 'encode'),
+    # Mixed Unicode
+    ('f...@example.com, Foo Bar <b...@example.com>, Fôo Baz 
<b...@example.com>',
+     'f...@example.com, Foo Bar <b...@example.com>, \n 
=?utf-8?q?F=C3=B4o_Baz?= <b...@example.com>', 'encode'),
+    ('f...@example.com, Foo Bar <b...@example.com>, Fôo Baz 
<b...@example.com>, "Quux, Foo" <q...@example.com>',
+     ('f...@example.com, Foo Bar <b...@example.com>, \n'
+      ' =?utf-8?q?F=C3=B4o_Baz?= <b...@example.com>, "Quux, Foo" 
<q...@example.com>'), 'encode'),
+    
('01234567890123456789012345678901234567890123456789012345678...@example.org, 
ä <f...@example.org>',
+     
('01234567890123456789012345678901234567890123456789012345678...@example.org, 
\n'
+      ' =?utf-8?q?=C3=A4?= <f...@example.org>'), 'encode'),
+    # Test for https://github.com/python/cpython/issues/100900
+    ('f...@example.com, Foo Bar <b...@example.com>, Fôo Baz 
<b...@example.com>, "Quûx, Foo" <q...@example.com>',
+     ('f...@example.com, Foo Bar <b...@example.com>, \n'
+      ' =?utf-8?q?F=C3=B4o_Baz?= <b...@example.com>, \n 
=?utf-8?q?Qu=C3=BBx=2C_Foo?= <q...@example.com>'), 'encode'),
+    # Test preserve
+    ('f...@example.com, Foo Bar <b...@example.com>, Fôo Baz 
<b...@example.com>, "Quûx, Foo" <q...@example.com>',
+     'f...@example.com, Foo Bar <b...@example.com>, Fôo Baz 
<b...@example.com>, \n "Quûx, Foo" <q...@example.com>',
+     'preserve'),
+    # Test decode
+    ('f...@example.com, Foo Bar <b...@example.com>, 
=?utf-8?q?Qu=C3=BBx=2C_Foo?= <q...@example.com>',
+     'f...@example.com, Foo Bar <b...@example.com>, \n "Quûx, Foo" 
<q...@example.com>',
+     'decode'),
 ])
-def test_sevenbitify_headers(sampledir, headers, verify):
-    msg = email.message.Message(policy=b4.emlpolicy)
-    for header, value in headers.items():
-        msg[header] = value
-    msg.set_payload('Unicôde Côntent in the body.\n', charset='utf-8')
-    msg.set_charset('utf-8')
-    msg = b4.sevenbitify_headers(msg)
-    odata = msg.as_bytes(policy=email.policy.SMTP)
-    vfile = os.path.join(sampledir, f'{verify}.verify')
-    with open(vfile, 'rb') as fh:
-        vdata = fh.read()
-        assert odata.decode() == vdata.decode()
+def test_header_wrapping(sampledir, hval, verify, tr):
+    hname = 'To' if '@' in hval else "X-Header"
+    wrapped = b4.LoreMessage.wrap_header((hname, hval), transform=tr)
+    assert wrapped.decode() == f'{hname}: {verify}'
+    wname, wval = wrapped.split(b':', maxsplit=1)
+    if tr != 'decode':
+        cval = b4.LoreMessage.clean_header(wval.decode())
+        assert cval == hval
+
+
+@pytest.mark.parametrize('pairs,verify,clean', [
+    ([('', 'f...@example.com'), ('Foo Bar', 'b...@example.com')],
+        'f...@example.com, Foo Bar <b...@example.com>', True),
+    ([('', 'f...@example.com'), ('Foo, Bar', 'b...@example.com')],
+        'f...@example.com, "Foo, Bar" <b...@example.com>', True),
+    ([('', 'f...@example.com'), ('Fôo, Bar', 'b...@example.com')],
+        'f...@example.com, "Fôo, Bar" <b...@example.com>', True),
+    ([('', 'f...@example.com'), ('=?utf-8?q?Qu=C3=BBx_Foo?=', 
'q...@example.com')],
+     'f...@example.com, Quûx Foo <q...@example.com>', True),
+    ([('', 'f...@example.com'), ('=?utf-8?q?Qu=C3=BBx=2C_Foo?=', 
'q...@example.com')],
+        'f...@example.com, "Quûx, Foo" <q...@example.com>', True),
+    ([('', 'f...@example.com'), ('=?utf-8?q?Qu=C3=BBx=2C_Foo?=', 
'q...@example.com')],
+     'f...@example.com, =?utf-8?q?Qu=C3=BBx=2C_Foo?= <q...@example.com>', 
False),
+])
+def test_format_addrs(pairs, verify, clean):
+    formatted = b4.format_addrs(pairs, clean)
+    assert formatted == verify

Reply via email to