예제 #1
0
def _getbundlemsgs(repo, sender, bundle, **opts):
    """Get the full email for sending a given bundle

    This function returns a list of "email" tuples (subject, content, None).
    The list is always one message long in that case.
    """
    ui = repo.ui
    _charsets = mail._charsets(ui)
    subj = opts.get('subject') or prompt(ui, b'Subject:',
                                         b'A bundle for your repository')

    body = _getdescription(repo, b'', sender, **opts)
    msg = emimemultipart.MIMEMultipart()
    if body:
        msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
    datapart = emimebase.MIMEBase('application', 'x-mercurial-bundle')
    datapart.set_payload(bundle)
    bundlename = b'%s.hg' % opts.get('bundlename', b'bundle')
    datapart.add_header(
        'Content-Disposition',
        'attachment',
        filename=encoding.strfromlocal(bundlename),
    )
    emailencoders.encode_base64(datapart)
    msg.attach(datapart)
    msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
    return [(msg, subj, None)]
예제 #2
0
파일: index.py 프로젝트: JesseDavids/mqtta
def _createdb(path):
    # print('open db', path)
    # import traceback
    # traceback.print_stack()
    db = sqlite3.connect(encoding.strfromlocal(path))
    db.text_factory = bytes

    res = db.execute('PRAGMA user_version').fetchone()[0]

    # New database.
    if res == 0:
        for statement in _SCHEMA.split(';'):
            db.execute(statement.strip())

        db.commit()

    elif res == _CURRENT_SCHEMA_VERSION:
        pass

    else:
        raise error.Abort(_(b'sqlite database has unrecognized version'))

    db.execute('PRAGMA journal_mode=WAL')

    return db
예제 #3
0
    def http_error_401(self, req, fp, code, msg, headers):
        """Enforces that any authentication performed is HTTP Basic
        Authentication.  No authentication is also acceptable.
        """
        authreq = headers.get(r'www-authenticate', None)
        if authreq:
            scheme = authreq.split()[0]

            if scheme.lower() != r'basic':
                msg = _(b'the server must support Basic Authentication')
                raise util.urlerr.httperror(req.get_full_url(), code,
                                            encoding.strfromlocal(msg),
                                            headers, fp)
        return None
예제 #4
0
def messageid(ctx, domain, messageidseed):
    if domain and messageidseed:
        host = domain
    else:
        host = encoding.strtolocal(socket.getfqdn())
    if messageidseed:
        messagehash = hashlib.sha512(ctx.hex() + messageidseed)
        messageid = b'<hg.%s@%s>' % (
            pycompat.sysbytes(messagehash.hexdigest()[:64]),
            host,
        )
    else:
        messageid = b'<hg.%s.%d.%d@%s>' % (
            ctx,
            int(time.time()),
            hash(ctx.repo().root),
            host,
        )
    return encoding.strfromlocal(messageid)
예제 #5
0
def makedb(path):
    """Construct a database handle for a database at path."""

    db = sqlite3.connect(encoding.strfromlocal(path))
    db.text_factory = bytes

    res = db.execute('PRAGMA user_version').fetchone()[0]

    # New database.
    if res == 0:
        for statement in CREATE_SCHEMA:
            db.execute(statement)

        db.commit()

    elif res == CURRENT_SCHEMA_VERSION:
        pass

    else:
        raise error.Abort(_(b'sqlite database has unrecognized version'))

    db.execute('PRAGMA journal_mode=WAL')

    return db
예제 #6
0
def _report_commit(ui, repo, ctx):
    domain = ui.config(b'notify_published', b'domain') or ui.config(
        b'notify', b'domain')
    messageidseed = ui.config(b'notify_published',
                              b'messageidseed') or ui.config(
                                  b'notify', b'messageidseed')
    template = ui.config(b'notify_published', b'template')
    spec = formatter.literal_templatespec(template)
    templater = logcmdutil.changesettemplater(ui, repo, spec)
    ui.pushbuffer()
    n = notify.notifier(ui, repo, b'incoming')

    subs = set()
    for sub, spec in n.subs:
        if spec is None:
            subs.add(sub)
            continue
        revs = repo.revs(b'%r and %d:', spec, ctx.rev())
        if len(revs):
            subs.add(sub)
            continue
    if len(subs) == 0:
        ui.debug(
            b'notify_published: no subscribers to selected repo and revset\n')
        return

    templater.show(
        ctx,
        changes=ctx.changeset(),
        baseurl=ui.config(b'web', b'baseurl'),
        root=repo.root,
        webroot=n.root,
    )
    data = ui.popbuffer()

    try:
        msg = mail.parsebytes(data)
    except emailerrors.MessageParseError as inst:
        raise error.Abort(inst)

    msg['In-reply-to'] = notify.messageid(ctx, domain, messageidseed)
    msg['Message-Id'] = notify.messageid(ctx, domain,
                                         messageidseed + b'-published')
    msg['Date'] = encoding.strfromlocal(
        dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2"))
    if not msg['From']:
        sender = ui.config(b'email', b'from') or ui.username()
        if b'@' not in sender or b'@localhost' in sender:
            sender = n.fixmail(sender)
        msg['From'] = mail.addressencode(ui, sender, n.charsets, n.test)
    msg['To'] = ', '.join(sorted(subs))

    msgtext = msg.as_bytes() if pycompat.ispy3 else msg.as_string()
    if ui.configbool(b'notify', b'test'):
        ui.write(msgtext)
        if not msgtext.endswith(b'\n'):
            ui.write(b'\n')
    else:
        ui.status(_(b'notify_published: sending mail for %d\n') % ctx.rev())
        mail.sendmail(ui,
                      emailutils.parseaddr(msg['From'])[1],
                      subs,
                      msgtext,
                      mbox=n.mbox)
예제 #7
0
def email(ui, repo, *revs, **opts):
    """send changesets by email

    By default, diffs are sent in the format generated by
    :hg:`export`, one per message. The series starts with a "[PATCH 0
    of N]" introduction, which describes the series as a whole.

    Each patch email has a Subject line of "[PATCH M of N] ...", using
    the first line of the changeset description as the subject text.
    The message contains two or three parts. First, the changeset
    description.

    With the -d/--diffstat option, if the diffstat program is
    installed, the result of running diffstat on the patch is inserted.

    Finally, the patch itself, as generated by :hg:`export`.

    With the -d/--diffstat or --confirm options, you will be presented
    with a final summary of all messages and asked for confirmation before
    the messages are sent.

    By default the patch is included as text in the email body for
    easy reviewing. Using the -a/--attach option will instead create
    an attachment for the patch. With -i/--inline an inline attachment
    will be created. You can include a patch both as text in the email
    body and as a regular or an inline attachment by combining the
    -a/--attach or -i/--inline with the --body option.

    With -B/--bookmark changesets reachable by the given bookmark are
    selected.

    With -o/--outgoing, emails will be generated for patches not found
    in the destination repository (or only those which are ancestors
    of the specified revisions if any are provided)

    With -b/--bundle, changesets are selected as for --outgoing, but a
    single email containing a binary Mercurial bundle as an attachment
    will be sent. Use the ``patchbomb.bundletype`` config option to
    control the bundle type as with :hg:`bundle --type`.

    With -m/--mbox, instead of previewing each patchbomb message in a
    pager or sending the messages directly, it will create a UNIX
    mailbox file with the patch emails. This mailbox file can be
    previewed with any mail user agent which supports UNIX mbox
    files.

    With -n/--test, all steps will run, but mail will not be sent.
    You will be prompted for an email recipient address, a subject and
    an introductory message describing the patches of your patchbomb.
    Then when all is done, patchbomb messages are displayed.

    In case email sending fails, you will find a backup of your series
    introductory message in ``.hg/last-email.txt``.

    The default behavior of this command can be customized through
    configuration. (See :hg:`help patchbomb` for details)

    Examples::

      hg email -r 3000          # send patch 3000 only
      hg email -r 3000 -r 3001  # send patches 3000 and 3001
      hg email -r 3000:3005     # send patches 3000 through 3005
      hg email 3000             # send patch 3000 (deprecated)

      hg email -o               # send all patches not in default
      hg email -o DEST          # send all patches not in DEST
      hg email -o -r 3000       # send all ancestors of 3000 not in default
      hg email -o -r 3000 DEST  # send all ancestors of 3000 not in DEST

      hg email -B feature       # send all ancestors of feature bookmark

      hg email -b               # send bundle of all patches not in default
      hg email -b DEST          # send bundle of all patches not in DEST
      hg email -b -r 3000       # bundle of all ancestors of 3000 not in default
      hg email -b -r 3000 DEST  # bundle of all ancestors of 3000 not in DEST

      hg email -o -m mbox &&    # generate an mbox file...
        mutt -R -f mbox         # ... and view it with mutt
      hg email -o -m mbox &&    # generate an mbox file ...
        formail -s sendmail \\   # ... and use formail to send from the mbox
          -bm -t < mbox         # ... using sendmail

    Before using this command, you will need to enable email in your
    hgrc. See the [email] section in hgrc(5) for details.
    """
    opts = pycompat.byteskwargs(opts)

    _charsets = mail._charsets(ui)

    bundle = opts.get(b'bundle')
    date = opts.get(b'date')
    mbox = opts.get(b'mbox')
    outgoing = opts.get(b'outgoing')
    rev = opts.get(b'rev')
    bookmark = opts.get(b'bookmark')

    if not (opts.get(b'test') or mbox):
        # really sending
        mail.validateconfig(ui)

    if not (revs or rev or outgoing or bundle or bookmark):
        raise error.Abort(
            _(b'specify at least one changeset with -B, -r or -o'))

    if outgoing and bundle:
        raise error.Abort(
            _(b"--outgoing mode always on with --bundle;"
              b" do not re-specify --outgoing"))
    cmdutil.check_at_most_one_arg(opts, b'rev', b'bookmark')

    if outgoing or bundle:
        if len(revs) > 1:
            raise error.Abort(_(b"too many destinations"))
        if revs:
            dest = revs[0]
        else:
            dest = None
        revs = []

    if rev:
        if revs:
            raise error.Abort(_(b'use only one form to specify the revision'))
        revs = rev
    elif bookmark:
        if bookmark not in repo._bookmarks:
            raise error.Abort(_(b"bookmark '%s' not found") % bookmark)
        revs = scmutil.bookmarkrevs(repo, bookmark)

    revs = scmutil.revrange(repo, revs)
    if outgoing:
        revs = _getoutgoing(repo, dest, revs)
    if bundle:
        opts[b'revs'] = [b"%d" % r for r in revs]

    # check if revision exist on the public destination
    publicurl = repo.ui.config(b'patchbomb', b'publicurl')
    if publicurl:
        repo.ui.debug(b'checking that revision exist in the public repo\n')
        try:
            publicpeer = hg.peer(repo, {}, publicurl)
        except error.RepoError:
            repo.ui.write_err(
                _(b'unable to access public repo: %s\n') % publicurl)
            raise
        if not publicpeer.capable(b'known'):
            repo.ui.debug(b'skipping existence checks: public repo too old\n')
        else:
            out = [repo[r] for r in revs]
            known = publicpeer.known(h.node() for h in out)
            missing = []
            for idx, h in enumerate(out):
                if not known[idx]:
                    missing.append(h)
            if missing:
                if len(missing) > 1:
                    msg = _(b'public "%s" is missing %s and %i others')
                    msg %= (publicurl, missing[0], len(missing) - 1)
                else:
                    msg = _(b'public url %s is missing %s')
                    msg %= (publicurl, missing[0])
                missingrevs = [ctx.rev() for ctx in missing]
                revhint = b' '.join(
                    b'-r %s' % h for h in repo.set(b'heads(%ld)', missingrevs))
                hint = _(b"use 'hg push %s %s'") % (publicurl, revhint)
                raise error.Abort(msg, hint=hint)

    # start
    if date:
        start_time = dateutil.parsedate(date)
    else:
        start_time = dateutil.makedate()

    def genmsgid(id):
        return _msgid(id[:20], int(start_time[0]))

    # deprecated config: patchbomb.from
    sender = (opts.get(b'from') or ui.config(b'email', b'from')
              or ui.config(b'patchbomb', b'from')
              or prompt(ui, b'From', ui.username()))

    if bundle:
        stropts = pycompat.strkwargs(opts)
        bundledata = _getbundle(repo, dest, **stropts)
        bundleopts = stropts.copy()
        bundleopts.pop('bundle', None)  # already processed
        msgs = _getbundlemsgs(repo, sender, bundledata, **bundleopts)
    else:
        msgs = _getpatchmsgs(repo, sender, revs, **pycompat.strkwargs(opts))

    showaddrs = []

    def getaddrs(header, ask=False, default=None):
        configkey = header.lower()
        opt = header.replace(b'-', b'_').lower()
        addrs = opts.get(opt)
        if addrs:
            showaddrs.append(b'%s: %s' % (header, b', '.join(addrs)))
            return mail.addrlistencode(ui, addrs, _charsets, opts.get(b'test'))

        # not on the command line: fallback to config and then maybe ask
        addr = ui.config(b'email', configkey) or ui.config(
            b'patchbomb', configkey)
        if not addr:
            specified = ui.hasconfig(b'email', configkey) or ui.hasconfig(
                b'patchbomb', configkey)
            if not specified and ask:
                addr = prompt(ui, header, default=default)
        if addr:
            showaddrs.append(b'%s: %s' % (header, addr))
            return mail.addrlistencode(ui, [addr], _charsets,
                                       opts.get(b'test'))
        elif default:
            return mail.addrlistencode(ui, [default], _charsets,
                                       opts.get(b'test'))
        return []

    to = getaddrs(b'To', ask=True)
    if not to:
        # we can get here in non-interactive mode
        raise error.Abort(_(b'no recipient addresses provided'))
    cc = getaddrs(b'Cc', ask=True, default=b'')
    bcc = getaddrs(b'Bcc')
    replyto = getaddrs(b'Reply-To')

    confirm = ui.configbool(b'patchbomb', b'confirm')
    confirm |= bool(opts.get(b'diffstat') or opts.get(b'confirm'))

    if confirm:
        ui.write(_(b'\nFinal summary:\n\n'), label=b'patchbomb.finalsummary')
        ui.write((b'From: %s\n' % sender), label=b'patchbomb.from')
        for addr in showaddrs:
            ui.write(b'%s\n' % addr, label=b'patchbomb.to')
        for m, subj, ds in msgs:
            ui.write((b'Subject: %s\n' % subj), label=b'patchbomb.subject')
            if ds:
                ui.write(ds, label=b'patchbomb.diffstats')
        ui.write(b'\n')
        if ui.promptchoice(
                _(b'are you sure you want to send (yn)?$$ &Yes $$ &No')):
            raise error.Abort(_(b'patchbomb canceled'))

    ui.write(b'\n')

    parent = opts.get(b'in_reply_to') or None
    # angle brackets may be omitted, they're not semantically part of the msg-id
    if parent is not None:
        parent = encoding.strfromlocal(parent)
        if not parent.startswith('<'):
            parent = '<' + parent
        if not parent.endswith('>'):
            parent += '>'

    sender_addr = eutil.parseaddr(encoding.strfromlocal(sender))[1]
    sender = mail.addressencode(ui, sender, _charsets, opts.get(b'test'))
    sendmail = None
    firstpatch = None
    progress = ui.makeprogress(_(b'sending'),
                               unit=_(b'emails'),
                               total=len(msgs))
    for i, (m, subj, ds) in enumerate(msgs):
        try:
            m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
            if not firstpatch:
                firstpatch = m['Message-Id']
            m['X-Mercurial-Series-Id'] = firstpatch
        except TypeError:
            m['Message-Id'] = genmsgid('patchbomb')
        if parent:
            m['In-Reply-To'] = parent
            m['References'] = parent
        if not parent or 'X-Mercurial-Node' not in m:
            parent = m['Message-Id']

        m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version().decode()
        m['Date'] = eutil.formatdate(start_time[0], localtime=True)

        start_time = (start_time[0] + 1, start_time[1])
        m['From'] = sender
        m['To'] = ', '.join(to)
        if cc:
            m['Cc'] = ', '.join(cc)
        if bcc:
            m['Bcc'] = ', '.join(bcc)
        if replyto:
            m['Reply-To'] = ', '.join(replyto)
        if opts.get(b'test'):
            ui.status(_(b'displaying '), subj, b' ...\n')
            ui.pager(b'email')
            generator = mail.Generator(ui, mangle_from_=False)
            try:
                generator.flatten(m, False)
                ui.write(b'\n')
            except IOError as inst:
                if inst.errno != errno.EPIPE:
                    raise
        else:
            if not sendmail:
                sendmail = mail.connect(ui, mbox=mbox)
            ui.status(_(b'sending '), subj, b' ...\n')
            progress.update(i, item=subj)
            if not mbox:
                # Exim does not remove the Bcc field
                del m['Bcc']
            fp = stringio()
            generator = mail.Generator(fp, mangle_from_=False)
            generator.flatten(m, False)
            alldests = to + bcc + cc
            sendmail(sender_addr, alldests, fp.getvalue())

    progress.complete()
예제 #8
0
def _msgid(node, timestamp):
    try:
        hostname = encoding.strfromlocal(encoding.environ[b'HGHOSTNAME'])
    except KeyError:
        hostname = socket.getfqdn()
    return '<%s.%d@%s>' % (node, timestamp, hostname)
예제 #9
0
def makepatch(
    ui,
    repo,
    rev,
    patchlines,
    opts,
    _charsets,
    idx,
    total,
    numbered,
    patchname=None,
):

    desc = []
    node = None
    body = b''

    for line in patchlines:
        if line.startswith(b'#'):
            if line.startswith(b'# Node ID'):
                node = line.split()[-1]
            continue
        if line.startswith(b'diff -r') or line.startswith(b'diff --git'):
            break
        desc.append(line)

    if not patchname and not node:
        raise ValueError

    if opts.get(b'attach') and not opts.get(b'body'):
        body = (b'\n'.join(desc[1:]).strip()
                or b'Patch subject is complete summary.')
        body += b'\n\n\n'

    if opts.get(b'plain'):
        while patchlines and patchlines[0].startswith(b'# '):
            patchlines.pop(0)
        if patchlines:
            patchlines.pop(0)
        while patchlines and not patchlines[0].strip():
            patchlines.pop(0)

    ds = patch.diffstat(patchlines)
    if opts.get(b'diffstat'):
        body += ds + b'\n\n'

    addattachment = opts.get(b'attach') or opts.get(b'inline')
    if not addattachment or opts.get(b'body'):
        body += b'\n'.join(patchlines)

    if addattachment:
        msg = emimemultipart.MIMEMultipart()
        if body:
            msg.attach(mail.mimeencode(ui, body, _charsets, opts.get(b'test')))
        p = mail.mimetextpatch(b'\n'.join(patchlines), 'x-patch',
                               opts.get(b'test'))
        binnode = bin(node)
        # if node is mq patch, it will have the patch file's name as a tag
        if not patchname:
            patchtags = [
                t for t in repo.nodetags(binnode)
                if t.endswith(b'.patch') or t.endswith(b'.diff')
            ]
            if patchtags:
                patchname = patchtags[0]
            elif total > 1:
                patchname = cmdutil.makefilename(repo[node],
                                                 b'%b-%n.patch',
                                                 seqno=idx,
                                                 total=total)
            else:
                patchname = cmdutil.makefilename(repo[node], b'%b.patch')
        disposition = r'inline'
        if opts.get(b'attach'):
            disposition = r'attachment'
        p['Content-Disposition'] = (disposition + '; filename=' +
                                    encoding.strfromlocal(patchname))
        msg.attach(p)
    else:
        msg = mail.mimetextpatch(body, display=opts.get(b'test'))

    prefix = _formatprefix(ui, repo, rev, opts.get(b'flag'), idx, total,
                           numbered)
    subj = desc[0].strip().rstrip(b'. ')
    if not numbered:
        subj = b' '.join([prefix, opts.get(b'subject') or subj])
    else:
        subj = b' '.join([prefix, subj])
    msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get(b'test'))
    msg['X-Mercurial-Node'] = pycompat.sysstr(node)
    msg['X-Mercurial-Series-Index'] = '%i' % idx
    msg['X-Mercurial-Series-Total'] = '%i' % total
    return msg, subj, ds
예제 #10
0
파일: churn.py 프로젝트: CJX32/my_blog
 def getkey(ctx):
     t, tz = ctx.date()
     date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
     return encoding.strtolocal(
         date.strftime(encoding.strfromlocal(opts[b'dateformat']))
     )
예제 #11
0
    def send(self, ctx, count, data):
        '''send message.'''

        # Select subscribers by revset
        subs = set()
        for sub, spec in self.subs:
            if spec is None:
                subs.add(sub)
                continue
            revs = self.repo.revs(b'%r and %d:', spec, ctx.rev())
            if len(revs):
                subs.add(sub)
                continue
        if len(subs) == 0:
            self.ui.debug(
                b'notify: no subscribers to selected repo and revset\n')
            return

        try:
            msg = mail.parsebytes(data)
        except emailerrors.MessageParseError as inst:
            raise error.Abort(inst)

        # store sender and subject
        sender = msg[r'From']
        subject = msg[r'Subject']
        if sender is not None:
            sender = mail.headdecode(sender)
        if subject is not None:
            subject = mail.headdecode(subject)
        del msg[r'From'], msg[r'Subject']

        if not msg.is_multipart():
            # create fresh mime message from scratch
            # (multipart templates must take care of this themselves)
            headers = msg.items()
            payload = msg.get_payload(decode=pycompat.ispy3)
            # for notification prefer readability over data precision
            msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
            # reinstate custom headers
            for k, v in headers:
                msg[k] = v

        msg[r'Date'] = encoding.strfromlocal(
            dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2"))

        # try to make subject line exist and be useful
        if not subject:
            if count > 1:
                subject = _(b'%s: %d new changesets') % (self.root, count)
            else:
                s = ctx.description().lstrip().split(b'\n', 1)[0].rstrip()
                subject = b'%s: %s' % (self.root, s)
        maxsubject = int(self.ui.config(b'notify', b'maxsubject'))
        if maxsubject:
            subject = stringutil.ellipsis(subject, maxsubject)
        msg[r'Subject'] = encoding.strfromlocal(
            mail.headencode(self.ui, subject, self.charsets, self.test))

        # try to make message have proper sender
        if not sender:
            sender = self.ui.config(b'email', b'from') or self.ui.username()
        if b'@' not in sender or b'@localhost' in sender:
            sender = self.fixmail(sender)
        msg[r'From'] = encoding.strfromlocal(
            mail.addressencode(self.ui, sender, self.charsets, self.test))

        msg[r'X-Hg-Notification'] = r'changeset %s' % ctx
        if not msg[r'Message-Id']:
            msg[r'Message-Id'] = messageid(ctx, self.domain,
                                           self.messageidseed)
        msg[r'To'] = encoding.strfromlocal(b', '.join(sorted(subs)))

        msgtext = msg.as_bytes() if pycompat.ispy3 else msg.as_string()
        if self.test:
            self.ui.write(msgtext)
            if not msgtext.endswith(b'\n'):
                self.ui.write(b'\n')
        else:
            self.ui.status(
                _(b'notify: sending %d subscribers %d changes\n') %
                (len(subs), count))
            mail.sendmail(
                self.ui,
                emailutils.parseaddr(msg[r'From'])[1],
                subs,
                msgtext,
                mbox=self.mbox,
            )
예제 #12
0
    def send(self, ctx, count, data):
        '''send message.'''

        # Select subscribers by revset
        subs = set()
        for sub, spec in self.subs:
            if spec is None:
                subs.add(sub)
                continue
            revs = self.repo.revs(b'%r and %d:', spec, ctx.rev())
            if len(revs):
                subs.add(sub)
                continue
        if len(subs) == 0:
            self.ui.debug(
                b'notify: no subscribers to selected repo and revset\n')
            return

        try:
            msg = mail.parsebytes(data)
        except emailerrors.MessageParseError as inst:
            raise error.Abort(inst)

        # store sender and subject
        sender = msg['From']
        subject = msg['Subject']
        if sender is not None:
            sender = mail.headdecode(sender)
        if subject is not None:
            subject = mail.headdecode(subject)
        del msg['From'], msg['Subject']

        if not msg.is_multipart():
            # create fresh mime message from scratch
            # (multipart templates must take care of this themselves)
            headers = msg.items()
            payload = msg.get_payload(decode=pycompat.ispy3)
            # for notification prefer readability over data precision
            msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
            # reinstate custom headers
            for k, v in headers:
                msg[k] = v

        msg['Date'] = encoding.strfromlocal(
            dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2"))

        # try to make subject line exist and be useful
        if not subject:
            if count > 1:
                subject = _(b'%s: %d new changesets') % (self.root, count)
            else:
                s = ctx.description().lstrip().split(b'\n', 1)[0].rstrip()
                subject = b'%s: %s' % (self.root, s)
        maxsubject = int(self.ui.config(b'notify', b'maxsubject'))
        if maxsubject:
            subject = stringutil.ellipsis(subject, maxsubject)
        msg['Subject'] = mail.headencode(self.ui, subject, self.charsets,
                                         self.test)

        # try to make message have proper sender
        if not sender:
            sender = self.ui.config(b'email', b'from') or self.ui.username()
        if b'@' not in sender or b'@localhost' in sender:
            sender = self.fixmail(sender)
        msg['From'] = mail.addressencode(self.ui, sender, self.charsets,
                                         self.test)

        msg['X-Hg-Notification'] = 'changeset %s' % ctx
        if not msg['Message-Id']:
            msg['Message-Id'] = messageid(ctx, self.domain, self.messageidseed)
        if self.reply:
            unfi = self.repo.unfiltered()
            has_node = unfi.changelog.index.has_node
            predecessors = [
                unfi[ctx2] for ctx2 in obsutil.allpredecessors(
                    unfi.obsstore, [ctx.node()])
                if ctx2 != ctx.node() and has_node(ctx2)
            ]
            if predecessors:
                # There is at least one predecessor, so which to pick?
                # Ideally, there is a unique root because changesets have
                # been evolved/rebased one step at a time. In this case,
                # just picking the oldest known changeset provides a stable
                # base. It doesn't help when changesets are folded. Any
                # better solution would require storing more information
                # in the repository.
                pred = min(predecessors, key=lambda ctx: ctx.rev())
                msg['In-Reply-To'] = messageid(pred, self.domain,
                                               self.messageidseed)
        msg['To'] = ', '.join(sorted(subs))

        msgtext = msg.as_bytes() if pycompat.ispy3 else msg.as_string()
        if self.test:
            self.ui.write(msgtext)
            if not msgtext.endswith(b'\n'):
                self.ui.write(b'\n')
        else:
            self.ui.status(
                _(b'notify: sending %d subscribers %d changes\n') %
                (len(subs), count))
            mail.sendmail(
                self.ui,
                emailutils.parseaddr(msg['From'])[1],
                subs,
                msgtext,
                mbox=self.mbox,
            )