On Sat, 22 Nov 2014, Charles Cazabon wrote:
> I hope Debian can simply accept the newer version of getmail; as I said, I try

The "next stable" version of Debian (aka "jessie") has getmail 4.46.0.

We also have an official backports archive, and it will also have getmail
4.46.0 in a few days.  This won't automatically update any of the users of
current Debian stable (aka "wheezy") to 4.46.0, but they can manually do so
if they so wish.

However, to get current Debian stable (wheezy) users automatically updated
to 4.46.0, we need a so called stable-update.  This needs an OK from the
stable release managers, and the non-trivial changes from 4.32.0 to 4.46.0
are a problem.

The key issue here is "risk of regression".  I have not seen any
regressions, and I have been using a 4.46.0 backport for a while already,
but we need to convince the stable release manager that the benefits of
going from 4.32.0 to 4.46.0 are worth the risk of regression, and that these
risks are low.

For the record, attached is an unified diff with the relevant (i.e.
excluding documentation) changes between 4.32.0 and 4.46.0.

-- 
  "One disk to rule them all, One disk to find them. One disk to bring
  them all and in the darkness grind them. In the Land of Redmond
  where the shadows lie." -- The Silicon Valley Tarot
  Henrique Holschuh
diff -ru getmail-4.32.0/docs/CHANGELOG getmail-4.46.0/docs/CHANGELOG
diff -ru getmail-4.32.0/docs/configuration.html getmail-4.46.0/docs/configuration.html
diff -ru getmail-4.32.0/docs/configuration.txt getmail-4.46.0/docs/configuration.txt
diff -ru getmail-4.32.0/docs/documentation.txt getmail-4.46.0/docs/documentation.txt
diff -ru getmail-4.32.0/docs/faq.html getmail-4.46.0/docs/faq.html
diff -ru getmail-4.32.0/docs/faq.txt getmail-4.46.0/docs/faq.txt
diff -ru getmail-4.32.0/docs/getmail.1 getmail-4.46.0/docs/getmail.1
diff -ru getmail-4.32.0/docs/troubleshooting.txt getmail-4.46.0/docs/troubleshooting.txt
diff -ru getmail-4.32.0/getmail getmail-4.46.0/getmail
--- getmail-4.32.0/getmail	2012-06-20 00:44:15.000000000 -0300
+++ getmail-4.46.0/getmail	2014-04-07 00:24:46.000000000 -0300
@@ -14,6 +14,18 @@
 import pprint
 from optparse import OptionParser, OptionGroup
 import socket
+import signal
+
+# Optional gnome-keyring integration
+try:
+    import gnomekeyring
+    import glib
+    glib.set_application_name('getmail')
+    # And test to see if it's actually available
+    if not gnomekeyring.is_available():
+        gnomekeyring = None
+except ImportError:
+    gnomekeyring = None
 
 options_bool = (
     'read_all',
@@ -22,9 +34,11 @@
     'received',
     'message_log_verbose',
     'message_log_syslog',
+    'fingerprint',
 )
 options_int = (
     'delete_after',
+    'delete_bigger_than',
     'max_message_size',
     'max_messages_per_session',
     'max_bytes_per_session',
@@ -45,7 +59,7 @@
         logging
     from getmailcore.exceptions import *
     from getmailcore.utilities import eval_bool, logfile, format_params, \
-        address_no_brackets, expand_user_vars
+        address_no_brackets, expand_user_vars, get_password
 except ImportError, o:
     sys.stderr.write('ImportError:  %s\n' % o)
     sys.exit(127)
@@ -62,6 +76,7 @@
     'read_all' : True,
     'delete' : False,
     'delete_after' : 0,
+    'delete_bigger_than' : 0,
     'max_message_size' : 0,
     'max_messages_per_session' : 0,
     'max_bytes_per_session' : 0,
@@ -71,8 +86,21 @@
     'message_log_verbose' : False,
     'message_log_syslog' : False,
     'logfile' : None,
+    'fingerprint' : False,
 }
 
+
+
+
+#######################################
+def convert_to_sigint(unused1, unused2):
+    """Catch a SIGTERM and raise a SIGINT so getmail exits normally and does
+    cleanup if killed with default signal.
+    """
+    raise KeyboardInterrupt('from signal')
+
+signal.signal(signal.SIGTERM, convert_to_sigint)
+
 #######################################
 def blurb():
     log.info('getmail version %s\n' % __version__)
@@ -80,7 +108,7 @@
              'GNU GPL version 2.\n')
 
 #######################################
-def go(configs):
+def go(configs, idle):
     """Main code.
 
     Returns True if all goes well, False if any error condition occurs.
@@ -88,7 +116,27 @@
     blurb()
     summary = []
     errorexit = False
+    idling = False
+
+    if len(configs) > 1 and idle:
+        log.info('more than one config file given with --idle, ignoring\n')
+        idle = False
+
     for (configfile, retriever, _filters, destination, options) in configs:
+        if options['read_all'] and not options['delete']:
+            if idle:
+                # This is a nonsense combination of options; every time the
+                # server returns from IDLE, all messages will be re-retrieved.
+                log.error('%s: IDLE, read_all, and not delete - bad '
+                          'combination, skipping\n' 
+                          % retriever)
+                continue
+            else:
+                # Slightly less nonsensical, but still weird.
+                log.warning('%s: read_all and not delete -- all messages will '
+                            'be retrieved each time getmail is run\n' 
+                            % retriever)
+            
         oplevel = options['verbose']
         logverbose = options['message_log_verbose']
         now = int(time.time())
@@ -98,14 +146,16 @@
         if options['message_log_syslog']:
             syslog.openlog('getmail', 0, syslog.LOG_MAIL)
         try:
-            log.info('%s:\n' % retriever)
-            logline = 'Initializing %s:' % retriever
-            if options['logfile'] and logverbose:
-                options['logfile'].write(logline)
-            if options['message_log_syslog'] and logverbose:
-                syslog.syslog(syslog.LOG_INFO, logline)
-            retriever.initialize(options)
-            destination.retriever_info(retriever)
+            if not idling:
+                log.info('%s:\n' % retriever)
+                logline = 'Initializing %s:' % retriever
+                if options['logfile'] and logverbose:
+                    options['logfile'].write(logline)
+                if options['message_log_syslog'] and logverbose:
+                    syslog.syslog(syslog.LOG_INFO, logline)
+                retriever.initialize(options)
+                destination.retriever_info(retriever)
+
             for mailbox in retriever.mailboxes:
                 if mailbox:
                     # For POP this is None and uninteresting
@@ -215,6 +265,12 @@
                         if options['delete'] and timestamp:
                             log.debug('    will delete\n')
                             delete = True
+                        
+                        if (options['delete_bigger_than'] 
+                                and size > options['delete_bigger_than']):
+                            log.debug('    bigger than %d, will delete\n'
+                                      % options['delete_bigger_than'])
+                            delete = True
 
                         if not retrieve and timestamp is None:
                             # We haven't retrieved this message.  Don't delete it.
@@ -269,6 +325,11 @@
         except StopIteration:
             pass
 
+        except KeyboardInterrupt, o:
+            log.warning('%s: user aborted\n' % configfile)
+            if options['logfile']:
+                options['logfile'].write('user aborted')
+
         except socket.timeout, o:
             errorexit = True
             retriever.abort()
@@ -333,6 +394,45 @@
             )
         log.debug('retriever %s finished\n' % retriever)
         try:
+            if idle and not retriever.supports_idle:
+                log.info('--idle given, but retriever does not support IDLE\n')
+                idle = False
+            if idle and sys.version_info < (2, 5, 0):
+                log.info('--idle requires Python 2.5 or higher\n')
+                idle = False
+
+            if idle and not errorexit:
+                # TODO
+                # Okay, so what should really happen here is that when go_idle
+                # returns, getmail should use the *existing* connection to check
+                # for new messages and then call go_idle again once that is
+                # done. The current code layout doesn't lend itself very well to
+                # that since the message download code is coupled with the
+                # connection setup/teardown code.
+                #
+                # Therefore, we do a bit of a hack.
+                # We add the current config back into configs, so that when the
+                # main for loop over configs runs again, it will find the same
+                # config again, and thus download the new messages and then go
+                # back to IDLEing. Since the return value of go_idle changes the
+                # value of idling, a failed connection will cause it to become
+                # False, which will make the main go() loop reconnect, which is
+                # what we want.
+                # Expunge and close the mailbox to  prevent the same messages
+                # being pulled again in some configurations.
+                retriever.close_mailbox()
+                try:
+                    idling = retriever.go_idle(idle)
+                    # Returned from idle
+                    configs.append(configs[0])
+                    continue
+                except KeyboardInterrupt, o:
+                    # Because configs isn't appended to, this just means we'll
+                    # quit, which is presumably what the user wanted
+                    # The newline is to clear the ^C shown in terminal
+                    log.info('\n')
+                    pass
+
             retriever.quit()
         except getmailOperationError, o:
             errorexit = True
@@ -376,6 +476,20 @@
             dest='trace', action='store_true', default=False,
             help='print extended trace information (extremely verbose)'
         )
+        parser.add_option(
+            '-i', '--idle',
+            dest='idle', action='store', default='',
+            help='maintain connection and listen for new messages in FOLDER. '
+                 'Only applies if a single rc file is given with a connection '
+                 'to an IMAP server that supports the IDLE command',
+            metavar='FOLDER'
+        )
+        if gnomekeyring:
+            parser.add_option(
+                '--store-password-in-gnome-keyring',
+                dest='store_gnome_keyring', action='store_true', default=False,
+                help='store the POP/IMAP password in the Gnome keyring'
+            )
         overrides = OptionGroup(
             parser, 'Overrides',
             'The following options override those specified in any '
@@ -387,6 +501,11 @@
             help='operate more verbosely (may be given multiple times)'
         )
         overrides.add_option(
+            '--fingerprint',
+            dest='override_fingerprint', action='store_true',
+            help='show SSL/TLS fingerprint and connection information'
+        )
+        overrides.add_option(
             '-q', '--quiet',
             dest='override_verbose', action='store_const',
             const=0,
@@ -473,6 +592,7 @@
                 'read_all' : defaults['read_all'],
                 'delete' : defaults['delete'],
                 'delete_after' : defaults['delete_after'],
+                'delete_bigger_than' : defaults['delete_bigger_than'],
                 'max_message_size' : defaults['max_message_size'],
                 'max_messages_per_session' :
                     defaults['max_messages_per_session'],
@@ -484,6 +604,7 @@
                 'message_log' : defaults['message_log'],
                 'message_log_verbose' : defaults['message_log_verbose'],
                 'message_log_syslog' : defaults['message_log_syslog'],
+                'fingerprint' : defaults['fingerprint'],
             }
             # Python's ConfigParser .getboolean() couldn't handle booleans in
             # the defaults. Submitted a patch; they fixed it a different way.
@@ -595,6 +716,30 @@
                 except getmailOperationError, o:
                     log.error('Error initializing retriever: %s\n' % o)
                     continue
+                
+                # Retriever is okay.  Check if user wants us to store the
+                # password in a Gnome keyring for future use.
+                if gnomekeyring and options.store_gnome_keyring:
+                    # Need to get the password first, if the user hasn't put
+                    # it in the rc file.
+                    if retriever.conf.get('password', None) is None:
+                        password = get_password(
+                            str(retriever), retriever.conf['username'], 
+                            retriever.conf['server'], retriever.received_with, 
+                            log
+                        )
+                    else:
+                        password = retriever.conf['password']
+
+                    gnomekeyring.set_network_password_sync(
+                        # keyring=None, user, domain=None, server, object=None, 
+                        # protocol, authtype=None, port=0
+                        None, retriever.conf['username'], None, 
+                        retriever.conf['server'], None, retriever.received_with,
+                        None, 0, password
+                    )
+                    log.info('Stored password in Gnome keyring.  Exiting.\n')
+                    raise SystemExit()
 
                 # Destination
                 log.debug('  getting destination\n')
@@ -674,7 +819,7 @@
                 )
 
             # Apply overrides from commandline
-            for option in ('read_all', 'delete', 'verbose'):
+            for option in ('read_all', 'delete', 'verbose', 'fingerprint'):
                 val = getattr(options, 'override_%s' % option)
                 if val is not None:
                     log.debug('overriding option %s from commandline %s\n'
@@ -718,7 +863,7 @@
             sys.exit()
 
         # Go!
-        success = go(configs)
+        success = go(configs, options.idle)
         if not success:
             raise SystemExit(127)
 
diff -ru getmail-4.32.0/getmailcore/destinations.py getmail-4.46.0/getmailcore/destinations.py
--- getmail-4.32.0/getmailcore/destinations.py	2010-06-29 22:02:07.000000000 -0300
+++ getmail-4.46.0/getmailcore/destinations.py	2013-01-27 17:21:35.000000000 -0200
@@ -81,6 +81,7 @@
         self.received_from = None
         self.received_with = None
         self.received_by = None
+        self.retriever = None
         self.log.trace('done\n')
 
     def retriever_info(self, retriever):
@@ -88,6 +89,7 @@
         self.received_from = retriever.received_from
         self.received_with = retriever.received_with
         self.received_by = retriever.received_by
+        self.retriever = retriever
 
     def deliver_message(self, msg, delivered_to=True, received=True):
         self.log.trace()
@@ -608,6 +610,9 @@
                     %(recipient) - recipient address
                     %(domain) - domain-part of recipient address
                     %(local) - local-part of recipient address
+                    %(mailbox) - for IMAP retrievers, the name of the 
+                        server-side mailbox/folder the message was retrieved
+                        from.  Will be empty for POP.
 
                   Warning: the text of these replacements is taken from the
                   message and is therefore under the control of a potential
@@ -691,6 +696,7 @@
                     'or GID 0 by default'
                 )
             args = [self.conf['path'], self.conf['path']]
+            msginfo['mailbox'] = self.retriever.mailbox_selected or ''
             for arg in self.conf['arguments']:
                 arg = expand_user_vars(arg)
                 for (key, value) in msginfo.items():
diff -ru getmail-4.32.0/getmailcore/filters.py getmail-4.46.0/getmailcore/filters.py
--- getmail-4.32.0/getmailcore/filters.py	2010-06-26 19:18:22.000000000 -0300
+++ getmail-4.46.0/getmailcore/filters.py	2013-08-03 17:27:04.000000000 -0300
@@ -343,6 +343,11 @@
 
         for line in [line.strip() for line in stdout.readlines()
                      if line.strip()]:
+            # Output from filter can be in any random text encoding and may
+            # not even be valid, which causes problems when trying to stick
+            # that text into message headers.  Try to decode it to something
+            # sane here first.
+            line = decode_crappy_text(line)
             msg.add_header('X-getmail-filter-classifier', line)
 
         return (exitcode, msg, err)
diff -ru getmail-4.32.0/getmailcore/__init__.py getmail-4.46.0/getmailcore/__init__.py
--- getmail-4.32.0/getmailcore/__init__.py	2012-07-06 17:00:33.000000000 -0300
+++ getmail-4.46.0/getmailcore/__init__.py	2014-04-07 00:24:46.000000000 -0300
@@ -16,7 +16,7 @@
     raise ImportError('getmail version 4 requires Python version 2.3.3'
                       ' or later')
 
-__version__ = '4.32.0'
+__version__ = '4.46.0'
 
 __all__ = [
     'baseclasses',
diff -ru getmail-4.32.0/getmailcore/message.py getmail-4.46.0/getmailcore/message.py
--- getmail-4.32.0/getmailcore/message.py	2011-07-16 14:28:47.000000000 -0300
+++ getmail-4.46.0/getmailcore/message.py	2013-08-03 17:27:04.000000000 -0300
@@ -7,20 +7,37 @@
     'Message',
 ]
 
+import sys
 import os
 import time
 import cStringIO
+import re
 import email
 import email.Errors
 import email.Utils
 import email.Parser
 from email.Generator import Generator
+try:
+    from email.header import Header
+except ImportError, o:
+    try:
+        from email.Header import Header
+    except ImportError, o:
+        # Python < 2.5
+        from email import Header
 
 from getmailcore.exceptions import *
 from getmailcore.utilities import mbox_from_escape, format_header, \
     address_no_brackets
 import getmailcore.logging
 
+if sys.hexversion < 0x02040000:
+    # email module in Python 2.3 uses more recursion to parse messages or
+    # similar; a user reported recursion errors with a message with ~300
+    # MIME parts.
+    # Hack around it by increasing the recursion limit.
+    sys.setrecursionlimit(2000)
+
 message_attributes = (
     'sender',
     'received_by',
@@ -29,6 +46,9 @@
     'recipient'
 )
 
+RE_FROMLINE = re.compile(r'^(>*From )', re.MULTILINE)
+
+
 #######################################
 def corrupt_message(why, fromlines=None, fromstring=None):
     log = getmailcore.logging.Logger()
@@ -130,19 +150,25 @@
         it by writing out what we need, letting the generator write out the
         message, splitting it into lines, and joining them with the platform
         EOL.
+        
+        Note on mangle_from: the Python email.Generator class apparently only
+        quotes "From ", not ">From " (i.e. it uses mboxo format instead of
+        mboxrd).  So we don't use its mangling, and do it by hand instead.
         '''
-        f = cStringIO.StringIO()
         if include_from:
-            # This needs to be written out first, so we can't rely on the
-            # generator
-            f.write('From %s %s' % (mbox_from_escape(self.sender),
-                                    time.asctime()) + os.linesep)
+            # Mbox-style From line, not rfc822 From: header field.
+            fromline = 'From %s %s' % (mbox_from_escape(self.sender),
+                                       time.asctime()) + os.linesep
+        else:
+            fromline = ''
         # Write the Return-Path: header
-        f.write(format_header('Return-Path', '<%s>' % self.sender))
+        rpline = format_header('Return-Path', '<%s>' % self.sender)
         # Remove previous Return-Path: header fields.
         del self.__msg['Return-Path']
         if delivered_to:
-            f.write(format_header('Delivered-To', self.recipient or 'unknown'))
+            dtline = format_header('Delivered-To', self.recipient or 'unknown')
+        else:
+            dtline = ''
         if received:
             content = 'from %s by %s with %s' % (
                 self.received_from, self.received_by, self.received_with
@@ -151,13 +177,20 @@
                 content += ' for <%s>' % self.recipient
             content += '; ' + time.strftime('%d %b %Y %H:%M:%S -0000',
                                             time.gmtime())
-            f.write(format_header('Received', content))
-        gen = Generator(f, mangle_from, 0)
+            receivedline = format_header('Received', content)
+        else:
+            receivedline = ''
         # From_ handled above, always tell the generator not to include it
         try:
+            tmpf = cStringIO.StringIO()
+            gen = Generator(tmpf, False, 0)
             gen.flatten(self.__msg, False)
-            f.seek(0)
-            return os.linesep.join(f.read().splitlines() + [''])
+            strmsg = tmpf.getvalue()
+            if mangle_from:
+                # do mboxrd-style "From " line quoting
+                strmsg = RE_FROMLINE.sub(r'>\1', strmsg)
+            return (fromline + rpline + dtline + receivedline 
+                    + os.linesep.join(strmsg.splitlines() + ['']))
         except TypeError, o:
             # email module chokes on some badly-misformatted messages, even
             # late during flatten().  Hope this is fixed in Python 2.4.
@@ -171,7 +204,7 @@
                                 include_from)
 
     def add_header(self, name, content):
-        self.__msg[name] = content.rstrip()
+        self.__msg[name] = Header(content.rstrip(), 'utf-8')
 
     def remove_header(self, name):
         del self.__msg[name]
diff -ru getmail-4.32.0/getmailcore/_retrieverbases.py getmail-4.46.0/getmailcore/_retrieverbases.py
--- getmail-4.32.0/getmailcore/_retrieverbases.py	2012-07-06 17:00:33.000000000 -0300
+++ getmail-4.46.0/getmailcore/_retrieverbases.py	2014-04-07 00:24:46.000000000 -0300
@@ -42,6 +42,7 @@
 import poplib
 import imaplib
 import re
+import select
 
 try:
     # do we have a recent pykerberos?
@@ -52,6 +53,124 @@
 except ImportError:
     pass
 
+# hashlib only present in python2.5, ssl in python2.6; used together
+# in SSL functionality below
+try:
+    import ssl
+except ImportError:
+    ssl = None
+try:
+    import hashlib
+except ImportError:
+    hashlib = None
+
+# If we have an ssl module:
+if ssl:
+    # Is it recent enough to have hostname matching (Python 3.2+)?
+    try:
+        ssl_match_hostname = ssl.match_hostname
+    except AttributeError:
+    # Running a Python with no hostname matching
+        def _dnsname_match(dn, hostname, max_wildcards=1):
+            """Matching according to RFC 6125, section 6.4.3
+            http://tools.ietf.org/html/rfc6125#section-6.4.3
+            """
+            pats = []
+            if not dn:
+                return False
+        
+            parts = dn.split(r'.')
+            leftmost = parts[0]
+            remainder = parts[1:]
+        
+            wildcards = leftmost.count('*')
+            if wildcards > max_wildcards:
+                # Issue #17980: avoid denials of service by refusing more
+                # than one wildcard per fragment.  A survery of established
+                # policy among SSL implementations showed it to be a
+                # reasonable choice.
+                raise getmailOperationError(
+                    "too many wildcards in certificate DNS name: " + repr(dn))
+        
+            # speed up common case w/o wildcards
+            if not wildcards:
+                return dn.lower() == hostname.lower()
+        
+            # RFC 6125, section 6.4.3, subitem 1.
+            # The client SHOULD NOT attempt to match a presented identifier
+            # in which the wildcard character comprises a label other than
+            # the left-most label.
+            if leftmost == '*':
+                # When '*' is a fragment by itself, it matches a non-empty
+                # dotless fragment.
+                pats.append('[^.]+')
+            elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
+                # RFC 6125, section 6.4.3, subitem 3.
+                # The client SHOULD NOT attempt to match a presented identifier
+                # where the wildcard character is embedded within an A-label or
+                # U-label of an internationalized domain name.
+                pats.append(re.escape(leftmost))
+            else:
+                # Otherwise, '*' matches any dotless string, e.g. www*
+                pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
+        
+            # add the remaining fragments, ignore any wildcards
+            for frag in remainder:
+                pats.append(re.escape(frag))
+        
+            pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
+            return pat.match(hostname)
+        
+        
+        def ssl_match_hostname(cert, hostname):
+            """Verify that *cert* (in decoded format as returned by
+            SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and
+            RFC 6125 rules are followed, but IP addresses are not accepted
+            for *hostname*.
+        
+            getmailOperationError is raised on failure. On success, the function
+            returns nothing.
+            """
+            if not cert:
+                raise ValueError("empty or no certificate, ssl_match_hostname "
+                                 "needs an SSL socket or SSL context with "
+                                 "either CERT_OPTIONAL or CERT_REQUIRED")
+            dnsnames = []
+            san = cert.get('subjectAltName', ())
+            for key, value in san:
+                if key == 'DNS':
+                    if _dnsname_match(value, hostname):
+                        return
+                    dnsnames.append(value)
+            if not dnsnames:
+                # The subject is only checked when there is no dNSName entry
+                # in subjectAltName
+                for sub in cert.get('subject', ()):
+                    for key, value in sub:
+                        # XXX according to RFC 2818, the most specific
+                        # Common Name must be used.
+                        if key == 'commonName':
+                            if _dnsname_match(value, hostname):
+                                return
+                            dnsnames.append(value)
+            if len(dnsnames) > 1:
+                raise getmailOperationError("hostname %s "
+                    "doesn't match either of %s"
+                    % (hostname, ', '.join(map(repr, dnsnames))))
+            elif len(dnsnames) == 1:
+                raise getmailOperationError("hostname %s "
+                    "doesn't match %s"
+                    % (hostname, dnsnames[0]))
+            else:
+                raise getmailOperationError("no appropriate commonName or "
+                    "subjectAltName fields were found")
+        
+try:
+    from email.header import decode_header
+except ImportError, o:
+    # python < 2.5
+    from email.Header import decode_header
+
 from getmailcore.compatibility import *
 from getmailcore.exceptions import *
 from getmailcore.constants import *
@@ -111,6 +230,9 @@
 EAI_FAIL = getattr(socket, 'EAI_FAIL', NO_OBJ)
 
 
+# Constant for POPSSL
+POP3_SSL_PORT = 995
+
 #
 # Mix-in classes
 #
@@ -119,6 +241,7 @@
 class POP3initMixIn(object):
     '''Mix-In class to do POP3 non-SSL initialization.
     '''
+    SSL = False
     def _connect(self):
         self.log.trace()
         try:
@@ -140,10 +263,60 @@
 
 
 #######################################
+class POP3_SSL_EXTENDED(poplib.POP3_SSL):
+    # Extended SSL support for POP3 (certificate checking, 
+    # fingerprint matching, cipher selection, etc.)
+
+    def __init__(self, host, port=POP3_SSL_PORT, keyfile=None,
+                 certfile=None, ssl_version=None, ca_certs=None,
+                 ssl_ciphers=None):
+        self.host = host
+        self.port = port
+        self.keyfile = keyfile
+        self.certfile = certfile
+        self.ssl_version = ssl_version
+        self.ca_certs = ca_certs
+        self.ssl_ciphers = ssl_ciphers
+
+        self.buffer = ''
+        msg = "getaddrinfo returns an empty list"
+        self.sock = None
+        for res in socket.getaddrinfo(self.host, self.port, 0,
+                                      socket.SOCK_STREAM):
+            (af, socktype, proto, canonname, sa) = res
+            try:
+                self.sock = socket.socket(af, socktype, proto)
+                self.sock.connect(sa)
+            except socket.error, msg:
+                if self.sock:
+                    self.sock.close()
+                self.sock = None
+                continue
+            break
+        if not self.sock:
+            raise socket.error(msg)
+        extra_args = {}
+        if self.ssl_version:
+            extra_args['ssl_version'] = self.ssl_version
+        if self.ca_certs:
+            extra_args['cert_reqs'] = ssl.CERT_REQUIRED
+            extra_args['ca_certs'] = self.ca_certs
+        if self.ssl_ciphers:
+            extra_args['ciphers'] = self.ssl_ciphers
+
+        self.file = self.sock.makefile('rb')
+        self.sslobj = ssl.wrap_socket(self.sock, self.keyfile,
+                                      self.certfile, **extra_args)
+        self._debugging = 0
+        self.welcome = self._getresp()
+
+
+#######################################
 class Py24POP3SSLinitMixIn(object):
     '''Mix-In class to do POP3 over SSL initialization with Python 2.4's
     poplib.POP3_SSL class.
     '''
+    SSL = True
     def _connect(self):
         self.log.trace()
         if not hasattr(socket, 'ssl'):
@@ -151,8 +324,39 @@
                 'SSL not supported by this installation of Python'
             )
         (keyfile, certfile) = check_ssl_key_and_cert(self.conf)
-        try:
-            if keyfile:
+        ca_certs = check_ca_certs(self.conf)
+        ssl_version = check_ssl_version(self.conf)
+        ssl_fingerprints = check_ssl_fingerprints(self.conf)
+        ssl_ciphers = check_ssl_ciphers(self.conf)
+        using_extended_certs_interface = False
+        try:
+            if ca_certs or ssl_version or ssl_ciphers:
+                using_extended_certs_interface = True
+                # Python 2.6 or higher required, use above class instead of
+                # vanilla stdlib one
+                msg = ''
+                if keyfile:
+                    msg += 'with keyfile %s, certfile %s' % (keyfile, certfile)
+                if ssl_version:
+                    if msg:
+                        msg += ', '
+                    msg += ('using protocol version %s'
+                            % self.conf['ssl_version'].upper())
+                if ca_certs:
+                    if msg:
+                        msg += ', '
+                    msg += 'with ca_certs %s' % ca_certs
+
+                self.log.trace(
+                    'establishing POP3 SSL connection to %s:%d %s'
+                    % (self.conf['server'], self.conf['port'], msg)
+                    + os.linesep
+                )
+                self.conn = POP3_SSL_EXTENDED(
+                    self.conf['server'], self.conf['port'], keyfile, certfile,
+                    ssl_version, ca_certs, ssl_ciphers
+                )
+            elif keyfile:
                 self.log.trace(
                     'establishing POP3 SSL connection to %s:%d with '
                     'keyfile %s, certfile %s'
@@ -170,6 +374,41 @@
                 self.conn = poplib.POP3_SSL(self.conf['server'],
                                             self.conf['port'])
             self.setup_received(self.conn.sock)
+            if ssl and hashlib:
+                sslobj = self.conn.sslobj
+                peercert = sslobj.getpeercert(True)
+                ssl_cipher = sslobj.cipher()
+                if ssl_cipher:
+                    ssl_cipher = '%s:%s:%s' % ssl_cipher
+                if not peercert:
+                    actual_hash = None
+                else:
+                    actual_hash = hashlib.sha256(peercert).hexdigest().lower()
+            else:
+                actual_hash = None
+                ssl_cipher = None
+
+            # Ensure cert is for server we're connecting to
+            if ssl and self.conf['ca_certs']:
+                ssl_match_hostname(self.conn.sslobj.getpeercert(),
+                               self.conf['server'])
+
+            if ssl_fingerprints:
+                if not actual_hash:
+                    raise getmailOperationError(
+                        'socket ssl_fingerprints mismatch (no cert provided)'
+                    )
+
+                any_matches = False
+                for expected_hash in ssl_fingerprints:
+                    if expected_hash == actual_hash:
+                        any_matches = True
+                if not any_matches:
+                    raise getmailOperationError(
+                        'socket ssl_fingerprints mismatch (got %s)'
+                        % actual_hash
+                    )
+
         except poplib.error_proto, o:
             raise getmailOperationError('POP error (%s)' % o)
         except socket.timeout:
@@ -191,6 +430,7 @@
     '''Mix-In class to do POP3 over SSL initialization with custom-implemented
     code to support SSL with Python 2.3's poplib.POP3 class.
     '''
+    SSL = True
     def _connect(self):
         self.log.trace()
         if not hasattr(socket, 'ssl'):
@@ -198,6 +438,15 @@
                 'SSL not supported by this installation of Python'
             )
         (keyfile, certfile) = check_ssl_key_and_cert(self.conf)
+        ca_certs = check_ca_certs(self.conf)
+        ssl_version = check_ssl_version(self.conf)
+        ssl_fingerprints = check_ssl_fingerprints(self.conf)
+        ssl_ciphers = check_ssl_ciphers(self.conf)
+        if ca_certs or ssl_version or ssl_ciphers or ssl_fingerprints:
+            raise getmailConfigurationError(
+                'SSL extended options are not supported by this'
+                 ' installation of Python'
+            )
         try:
             if keyfile:
                 self.log.trace(
@@ -216,6 +465,7 @@
                     + os.linesep
                 )
                 self.conn = POP3SSL(self.conf['server'], self.conf['port'])
+
             self.setup_received(self.conn.rawsock)
         except poplib.error_proto, o:
             raise getmailOperationError('POP error (%s)' % o)
@@ -237,6 +487,7 @@
 class IMAPinitMixIn(object):
     '''Mix-In class to do IMAP non-SSL initialization.
     '''
+    SSL = False
     def _connect(self):
         self.log.trace()
         try:
@@ -254,11 +505,41 @@
                        + os.linesep)
 
 
+#######################################
+class IMAP4_SSL_EXTENDED(imaplib.IMAP4_SSL):
+    # Similar to above, but with extended support for SSL certificate checking,
+    # fingerprints, etc.
+    def __init__(self, host='', port=imaplib.IMAP4_SSL_PORT, keyfile=None, 
+                 certfile=None, ssl_version=None, ca_certs=None, 
+                 ssl_ciphers=None):
+       self.ssl_version = ssl_version
+       self.ca_certs = ca_certs
+       self.ssl_ciphers = ssl_ciphers
+       imaplib.IMAP4_SSL.__init__(self, host, port, keyfile, certfile)
+
+    def open(self, host='', port=imaplib.IMAP4_SSL_PORT):
+       self.host = host
+       self.port = port
+       self.sock = socket.create_connection((host, port))
+       extra_args = {}
+       if self.ssl_version:
+           extra_args['ssl_version'] = self.ssl_version
+       if self.ca_certs:
+           extra_args['cert_reqs'] = ssl.CERT_REQUIRED
+           extra_args['ca_certs'] = self.ca_certs
+       if self.ssl_ciphers:
+           extra_args['ciphers'] = self.ssl_ciphers
+
+       self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, 
+                                     **extra_args)
+       self.file = self.sslobj.makefile('rb')
+
 
 #######################################
 class IMAPSSLinitMixIn(object):
     '''Mix-In class to do IMAP over SSL initialization.
     '''
+    SSL = True
     def _connect(self):
         self.log.trace()
         if not hasattr(socket, 'ssl'):
@@ -266,8 +547,39 @@
                 'SSL not supported by this installation of Python'
             )
         (keyfile, certfile) = check_ssl_key_and_cert(self.conf)
-        try:
-            if keyfile:
+        ca_certs = check_ca_certs(self.conf)
+        ssl_version = check_ssl_version(self.conf)
+        ssl_fingerprints = check_ssl_fingerprints(self.conf)
+        ssl_ciphers = check_ssl_ciphers(self.conf)
+        using_extended_certs_interface = False
+        try:
+            if ca_certs or ssl_version or ssl_ciphers:
+                using_extended_certs_interface = True
+                # Python 2.6 or higher required, use above class instead of
+                # vanilla stdlib one
+                msg = ''
+                if keyfile:
+                    msg += 'with keyfile %s, certfile %s' % (keyfile, certfile)
+                if ssl_version:
+                    if msg:
+                        msg += ', '
+                    msg += ('using protocol version %s' 
+                            % self.conf['ssl_version'].upper())
+                if ca_certs:
+                    if msg:
+                        msg += ', '
+                    msg += 'with ca_certs %s' % ca_certs
+
+                self.log.trace(
+                    'establishing IMAP SSL connection to %s:%d %s'
+                    % (self.conf['server'], self.conf['port'], msg)
+                    + os.linesep
+                )
+                self.conn = IMAP4_SSL_EXTENDED(
+                    self.conf['server'], self.conf['port'], keyfile, certfile, 
+                    ssl_version, ca_certs, ssl_ciphers
+                )
+            elif keyfile:
                 self.log.trace(
                     'establishing IMAP SSL connection to %s:%d with keyfile '
                     '%s, certfile %s'
@@ -286,6 +598,41 @@
                 self.conn = imaplib.IMAP4_SSL(self.conf['server'],
                                               self.conf['port'])
             self.setup_received(self.conn.sock)
+            if ssl and hashlib:
+                sslobj = self.conn.ssl()
+                peercert = sslobj.getpeercert(True)
+                ssl_cipher = sslobj.cipher()
+                if ssl_cipher:
+                    ssl_cipher = '%s:%s:%s' % ssl_cipher
+                if not peercert:
+                    actual_hash = None
+                else:
+                    actual_hash = hashlib.sha256(peercert).hexdigest().lower()
+            else:
+                actual_hash = None
+                ssl_cipher = None
+
+            # Ensure cert is for server we're connecting to
+            if ssl and self.conf['ca_certs']:
+                ssl_match_hostname(self.conn.ssl().getpeercert(),
+                               self.conf['server'])
+
+            if ssl_fingerprints:
+                if not actual_hash:
+                    raise getmailOperationError(
+                        'socket ssl_fingerprints mismatch (no cert provided)'
+                    )
+
+                any_matches = False
+                for expected_hash in ssl_fingerprints:
+                    if expected_hash == actual_hash:
+                        any_matches = True
+                if not any_matches:
+                    raise getmailOperationError(
+                        'socket ssl_fingerprints mismatch (got %s)' 
+                        % actual_hash
+                    )
+
         except imaplib.IMAP4.error, o:
             raise getmailOperationError('IMAP error (%s)' % o)
         except socket.timeout:
@@ -305,14 +652,25 @@
                     % (self.conf['server'], o)
                 )
             else:
-                raise getmailOperationError('socket error during connect (%s)' % o)
+                raise getmailOperationError('socket error during connect (%s)' 
+                                            % o)
         except socket.sslerror, o:
             raise getmailOperationError(
                 'socket sslerror during connect (%s)' % o
             )
 
-        self.log.trace('IMAP SSL connection %s established' % self.conn
-                       + os.linesep)
+        fingerprint_message = ('IMAP SSL connection %s established'
+                               % self.conn)
+        if actual_hash:
+            fingerprint_message += ' with fingerprint %s' % actual_hash
+        if ssl_cipher:
+            fingerprint_message += ' using cipher %s' % ssl_cipher
+        fingerprint_message += os.linesep
+
+        if self.app_options['fingerprint']:
+            self.log.info(fingerprint_message)
+        else:
+            self.log.trace(fingerprint_message)
 
 #
 # Base classes
@@ -330,7 +688,7 @@
                      - name - parameter name
                      - type - a type function to compare the parameter value
                      against (i.e. str, int, bool)
-                     - default - optional default value.  If not preseent, the
+                     - default - optional default value.  If not present, the
                      parameter is required.
 
       __str__(self) - return a simple string representing the class instance.
@@ -372,7 +730,6 @@
       initialize(self, options)
       checkconf(self)
     '''
-
     def __init__(self, **args):
         self.headercache = {}
         self.deleted = {}
@@ -382,6 +739,7 @@
         self.gotmsglist = False
         self._clear_state()
         self.conn = None
+        self.supports_idle = False
         ConfigurableBase.__init__(self, **args)
 
     def _clear_state(self):
@@ -433,15 +791,16 @@
                 or (isinstance(mailbox, (str, unicode)) and mailbox)), (
             'bad mailbox %s (%s)' % (mailbox, type(mailbox))
         )
-        if isinstance(mailbox, str):
-            mailbox = mailbox.decode('utf-8')
         filename = self.oldmail_filename
-        if mailbox is None:
-            # No mailbox (POP), use above with no extension
-            pass
-        else:
+        if mailbox is not None:
+            if isinstance(mailbox, str):
+                mailbox = mailbox.decode('utf-8')
+            mailbox = re.sub(STRIP_CHAR_RE, '.', mailbox)
+            mailbox = mailbox.encode('utf-8')
             # Use oldmail file per IMAP folder
-            filename += '-' + re.sub(STRIP_CHAR_RE, '.', mailbox)
+            filename += '-' + mailbox
+        # else:
+            # mailbox is None, is POP, just use filename
         return filename
 
     def oldmail_exists(self, mailbox):
@@ -629,6 +988,10 @@
             for (i, line) in enumerate(msglist):
                 try:
                     (msgnum, msgid) = line.split(None, 1)
+                    # Don't allow / in UIDs we store, as we look for that to 
+                    # detect old-style oldmail files.  Shouldn't occur in POP3
+                    # anyway.
+                    msgid = msgid.replace('/', '-')
                 except ValueError:
                     # Line didn't contain two tokens.  Server is broken.
                     raise getmailOperationError(
@@ -702,7 +1065,7 @@
             response, lines, octets = self.conn.retr(msgnum)
             self.log.debug('RETR response "%s", %d octets'
                            % (response, octets) + os.linesep)
-            msg = Message(fromlines=lines)
+            msg = Message(fromlines=lines+[''])
             return msg
         except poplib.error_proto, o:
             raise getmailRetrievalError(
@@ -724,8 +1087,8 @@
         # Handle password
         if self.conf.get('password', None) is None:
             self.conf['password'] = get_password(
-                self, self.conf['username'], self.conf['server'], 'pop3', 
-                self.log
+                self, self.conf['username'], self.conf['server'],
+                self.received_with, self.log
             )
         RetrieverSkeleton.initialize(self, options)
         try:
@@ -995,18 +1358,33 @@
                                             % g['mailbox'])
         return mailboxes
 
+    def close_mailbox(self):
+        # Close current mailbox so deleted mail is expunged.  One getmail
+        # user had a buggy IMAP server that didn't do the automatic expunge,
+        # so we do it explicitly here.
+        self.conn.expunge()
+        self.conn.close()
+        self.write_oldmailfile(self.mailbox_selected)
+        # And clear some state
+        self.mailbox_selected = False
+        self.mailbox = None
+        self.uidvalidity = None
+        self.msgnum_by_msgid = {}
+        self.msgid_by_msgnum = {}
+        self.sorted_msgnum_msgid = ()
+        self._mboxuids = {}
+        self._mboxuidorder = []
+        self.msgsizes = {}
+        self.oldmail = {}
+        self.__delivered = {}
+
     def select_mailbox(self, mailbox):
         self.log.trace()
         assert mailbox in self.mailboxes, (
             'mailbox not in config (%s)' % mailbox
         )
         if self.mailbox_selected is not False:
-            # Close current mailbox so deleted mail is expunged.
-            # Except one user reports that an explicit expunge is needed with 
-            # his server, or else the mail is never removed from the mailbox.
-            self.conn.expunge()
-            self.conn.close()
-            self.write_oldmailfile(self.mailbox_selected)
+            self.close_mailbox()
 
         self._clear_state()
 
@@ -1015,7 +1393,8 @@
 
         self.log.debug('selecting mailbox "%s"' % mailbox + os.linesep)
         try:
-            if self.app_options['delete'] or self.app_options['delete_after']:
+            if (self.app_options['delete'] or self.app_options['delete_after'] 
+                    or self.app_options['delete_bigger_than']):
                 read_only = False
             else:
                 read_only = True
@@ -1055,9 +1434,11 @@
                 )
                 for line in response:
                     r = self._parse_imapattrresponse(line)
-                    msgid = (
-                        '%s/%s' % (self.uidvalidity, r['uid'])
-                    )
+                    # Don't allow / in UIDs we store, as we look for that to 
+                    # detect old-style oldmail files.  Can occur with IMAP, at 
+                    # least with some servers.
+                    uid = r['uid'].replace('/', '-')
+                    msgid = '%s/%s' % (self.uidvalidity, uid)
                     self._mboxuids[msgid] = r['uid']
                     self._mboxuidorder.append(msgid)
                     self.msgnum_by_msgid[msgid] = None
@@ -1144,11 +1525,62 @@
                 # response[0] is None instead of a message tuple
                 raise getmailRetrievalError('failed to retrieve msgid %s' 
                                             % msgid)
+
+            # record mailbox retrieved from in a header
+            msg.add_header('X-getmail-retrieved-from-mailbox', 
+                           self.mailbox_selected)
+
+            # google extensions: apply labels, etc
+            if 'X-GM-EXT-1' in self.conn.capabilities:
+                metadata = self._getgmailmetadata(uid, msg)
+                for (header, value) in metadata.items():
+                    msg.add_header(header, value)
+
             return msg
 
         except imaplib.IMAP4.error, o:
             raise getmailOperationError('IMAP error (%s)' % o)
 
+    def _getgmailmetadata(self, uid, msg):
+        """
+        Add Gmail labels and other metadata which Google exposes through an
+        IMAP extension to headers in the message.
+        
+        See https://developers.google.com/google-apps/gmail/imap_extensions
+        """
+        try:
+            # ['976 (X-GM-THRID 1410134259107225671 X-GM-MSGID '
+            #   '1410134259107225671 X-GM-LABELS (labels space '
+            #   'separated) UID 167669)']
+            response = self._parse_imapuidcmdresponse('FETCH', uid,
+                '(X-GM-LABELS X-GM-THRID X-GM-MSGID)')
+        except imaplib.IMAP4.error, o:
+            self.log.warning('Could not fetch google imap extensions: %s' % o)
+            return {}
+
+        if not response:
+            return {}
+            
+        ext = re.search(
+            'X-GM-THRID (?P<THRID>\d+) X-GM-MSGID (?P<MSGID>\d+)'
+            ' X-GM-LABELS \((?P<LABELS>.*)\) UID',
+            response[0]
+        )
+        if not ext:
+            self.log.warning(
+                'Could not parse google imap extensions. Server said: %s'
+                % repr(response)
+            )
+            return {}
+
+        results = ext.groupdict()
+        metadata = {}
+        for item in ('LABELS', 'THRID', 'MSGID'):
+            if item in results and results[item]:
+                metadata['X-GMAIL-%s' % item] = results[item]
+
+        return metadata
+
     def _getmsgbyid(self, msgid):
         self.log.trace()
         if self.conf.get('use_peek', True):
@@ -1172,8 +1604,8 @@
         if (self.conf.get('password', None) is None
                 and not (HAVE_KERBEROS_GSS and self.conf['use_kerberos'])):
             self.conf['password'] = get_password(
-                self, self.conf['username'], self.conf['server'], 'imap',
-                self.log
+                self, self.conf['username'], self.conf['server'], 
+                self.received_with, self.log
             )
             
         RetrieverSkeleton.initialize(self, options)
@@ -1216,7 +1648,22 @@
                                    + os.linesep)
                     del self.oldmail[msgid]
             """
-            
+            # Some IMAP servers change the available capabilities after 
+            # authentication, i.e. they present a limited set before login.
+            # The Python stlib IMAP4 class doesn't take this into account
+            # and just checks the capabilities immediately after connecting.
+            # Force a re-check now that we've authenticated.
+            (typ, dat) = self.conn.capability()
+            if dat == [None]:
+                # No response, don't update the stored capabilities
+                self.log.warn('no post-login CAPABILITY response from server\n')
+            else:
+                self.conn.capabilities = tuple(dat[-1].upper().split())
+
+            if 'IDLE' in self.conn.capabilities:
+                self.supports_idle = True
+                imaplib.Commands['IDLE'] = ('AUTH', 'SELECTED')
+
             if self.mailboxes == ('ALL', ):
                 # Special value meaning all mailboxes in account
                 self.mailboxes = tuple(self.list_mailboxes())
@@ -1235,18 +1682,79 @@
             pass
         self.conn = None
 
+    def go_idle(self, folder, timeout=300):
+        """Initiates IMAP's IDLE mode if the server supports it
+
+        Waits until state of current mailbox changes, and then returns. Returns
+        True if the connection still seems to be up, False otherwise.
+
+        May throw getmailOperationError if the server refuses the IDLE setup
+        (e.g. if the server does not support IDLE)
+
+        Default timeout is 5 minutes.
+        """
+
+        if not self.supports_idle:
+            self.log.warning('IDLE not supported, so not idling\n')
+            raise getmailOperationError(
+                'IMAP4 IDLE requested, but not supported by server'
+            )
+
+
+        if self.SSL:
+            sock = self.conn.ssl()
+        else:
+            sock = self.conn.socket()
+
+        # Based on current imaplib IDLE patch: http://bugs.python.org/issue11245
+        self.conn.untagged_responses = {}
+        self.conn.select(folder)
+        tag = self.conn._command('IDLE')
+        data = self.conn._get_response() # read continuation response
+
+        if data is not None:
+            raise getmailOperationError(
+                'IMAP4 IDLE requested, but server refused IDLE request: %s' 
+                % data
+            )
+
+        self.log.debug('Entering IDLE mode (server says "%s")\n' 
+                       % self.conn.continuation_response)
+
+        try:
+            aborted = None
+            (readable, unused, unused) = select.select([sock], [], [], timeout)
+        except KeyboardInterrupt, o:
+            # Delay raising this until we've stopped IDLE mode
+            aborted = o
+
+        if aborted is not None:
+            self.log.debug('IDLE mode cancelled\n')
+        elif readable:
+            # The socket has data waiting; server has updated status
+            self.log.info('IDLE message received\n')
+        else:
+            self.log.debug('IDLE timeout (%ds)\n' % timeout)
+
+        try:
+            self.conn.untagged_responses = {}
+            self.conn.send('DONE\r\n')
+            self.conn._command_complete('IDLE', tag)
+        except imaplib.IMAP4.error, o:
+            return False
+
+        if aborted:
+            raise aborted
+
+        return True
+
     def quit(self):
         self.log.trace()
         if not self.conn:
             return
         try:
             if self.mailbox_selected is not False:
-                # Close current mailbox so deleted mail is expunged.
-                # Except one user reports that an explicit expunge is needed 
-                # with his server, or else the mail is never removed from the 
-                # mailbox.
-                self.conn.expunge()
-                self.conn.close()
+                self.close_mailbox()
             self.conn.logout()
         except imaplib.IMAP4.error, o:
             #raise getmailOperationError('IMAP error (%s)' % o)
@@ -1296,13 +1804,14 @@
         self.log.trace()
         msg = IMAPRetrieverBase._getmsgbyid(self, msgid)
         data = {}
-        for (name, val) in msg.headers():
+        for (name, encoded_value) in msg.headers():
             name = name.lower()
-            val = val.strip()
-            if name in data:
-                data[name].append(val)
-            else:
-                data[name] = [val]
+            for (val, encoding) in decode_header(encoded_value):
+                val = val.strip()
+                if name in data:
+                    data[name].append(val)
+                else:
+                    data[name] = [val]
 
         try:
             line = data[self.envrecipname][self.envrecipnum]
diff -ru getmail-4.32.0/getmailcore/retrievers.py getmail-4.46.0/getmailcore/retrievers.py
--- getmail-4.32.0/getmailcore/retrievers.py	2012-06-21 11:21:17.000000000 -0300
+++ getmail-4.46.0/getmailcore/retrievers.py	2014-04-07 00:24:46.000000000 -0300
@@ -99,6 +99,10 @@
         ConfBool(name='delete_dup_msgids', required=False, default=False),
         ConfFile(name='keyfile', required=False, default=None),
         ConfFile(name='certfile', required=False, default=None),
+        ConfFile(name='ca_certs', required=False, default=None),
+        ConfTupleOfStrings(name='ssl_fingerprints', required=False, default=()),
+        ConfString(name='ssl_version', required=False, default=None),
+        ConfString(name='ssl_ciphers', required=False, default=None),
     )
     received_from = None
     received_with = 'POP3-SSL'
@@ -200,6 +204,10 @@
         ConfBool(name='use_apop', required=False, default=False),
         ConfFile(name='keyfile', required=False, default=None),
         ConfFile(name='certfile', required=False, default=None),
+        ConfFile(name='ca_certs', required=False, default=None),
+        ConfTupleOfStrings(name='ssl_fingerprints', required=False, default=()),
+        ConfString(name='ssl_version', required=False, default=None),
+        ConfString(name='ssl_ciphers', required=False, default=None),
     )
     received_with = 'POP3-SSL'
 
@@ -266,6 +274,10 @@
         ConfString(name='envelope_recipient'),
         ConfFile(name='keyfile', required=False, default=None),
         ConfFile(name='certfile', required=False, default=None),
+        ConfFile(name='ca_certs', required=False, default=None),
+        ConfTupleOfStrings(name='ssl_fingerprints', required=False, default=()),
+        ConfString(name='ssl_version', required=False, default=None),
+        ConfString(name='ssl_ciphers', required=False, default=None),
     )
     received_from = None
     received_with = 'POP3-SSL'
@@ -402,6 +414,10 @@
         ConfString(name='move_on_delete', required=False, default=None),
         ConfFile(name='keyfile', required=False, default=None),
         ConfFile(name='certfile', required=False, default=None),
+        ConfFile(name='ca_certs', required=False, default=None),
+        ConfTupleOfStrings(name='ssl_fingerprints', required=False, default=()),
+        ConfString(name='ssl_version', required=False, default=None),
+        ConfString(name='ssl_ciphers', required=False, default=None),
         # imaplib.IMAP4.login_cram_md5() requires the (unimplemented)
         # .authenticate(), so we can't do this yet (?).
         ConfBool(name='use_cram_md5', required=False, default=False),
@@ -484,6 +500,10 @@
         ConfString(name='move_on_delete', required=False, default=None),
         ConfFile(name='keyfile', required=False, default=None),
         ConfFile(name='certfile', required=False, default=None),
+        ConfFile(name='ca_certs', required=False, default=None),
+        ConfTupleOfStrings(name='ssl_fingerprints', required=False, default=()),
+        ConfString(name='ssl_version', required=False, default=None),
+        ConfString(name='ssl_ciphers', required=False, default=None),
         # imaplib.IMAP4.login_cram_md5() requires the (unimplemented)
         # .authenticate(), so we can't do this yet (?).
         ConfBool(name='use_cram_md5', required=False, default=False),
diff -ru getmail-4.32.0/getmailcore/utilities.py getmail-4.46.0/getmailcore/utilities.py
--- getmail-4.32.0/getmailcore/utilities.py	2012-02-01 19:37:20.000000000 -0200
+++ getmail-4.46.0/getmailcore/utilities.py	2013-10-24 23:08:51.000000000 -0200
@@ -6,7 +6,13 @@
     'address_no_brackets',
     'change_usergroup',
     'change_uidgid',
+    'decode_crappy_text',
+    'format_header',
     'check_ssl_key_and_cert',
+    'check_ca_certs',
+    'check_ssl_version',
+    'check_ssl_fingerprints',
+    'check_ssl_ciphers',
     'deliver_maildir',
     'eval_bool',
     'expand_user_vars',
@@ -37,6 +43,27 @@
 import grp
 import getpass
 import commands
+import sys
+
+# hashlib only present in python2.5, ssl in python2.6; used together
+# in SSL functionality below
+try:
+    import ssl
+except ImportError:
+    ssl = None
+try:
+    import hashlib
+except ImportError:
+    hashlib = None
+
+# Optional gnome-keyring integration
+try:
+    import gnomekeyring
+    # And test to see if it's actually available
+    if not gnomekeyring.is_available():
+        gnomekeyring = None
+except ImportError:
+    gnomekeyring = None
 
 from getmailcore.exceptions import *
 
@@ -411,6 +438,29 @@
                                    % (uid, gid, o))
 
 #######################################
+def decode_crappy_text(s):
+    '''Take a line of text in arbitrary and possibly broken bytestring encoding
+    and return an ASCII or unicode version of it.
+    '''
+    # first, assume it was written in the encoding of the user's terminal
+    lang = os.environ.get('LANG')
+    if lang:
+        try:
+            (lang, encoding) = lang.split('.')
+            return s.decode(encoding)
+        except (UnicodeError, ValueError), o:
+            pass
+    # that failed; try well-formed in various common encodings next
+    for encoding in ('ascii', 'utf-8', 'latin-1', 'utf-16'):
+        try:
+            return s.decode(encoding)
+        except UnicodeError, o:
+            continue
+    # all failed - force it
+    return s.decode('utf-8', 'replace')
+    
+
+#######################################
 def format_header(name, line):
     '''Take a long line and return rfc822-style multiline header.
     '''
@@ -475,43 +525,169 @@
         )
     return (keyfile, certfile)
 
+#######################################
+def check_ca_certs(conf):
+    ca_certs = conf['ca_certs']
+    if ca_certs is not None:
+        ca_certs = expand_user_vars(ca_certs)
+        if ssl is None:
+            raise getmailConfigurationError(
+                'specifying ca_certs not supported by this installation of '
+                'Python; requires Python 2.6'
+            )
+    if ca_certs and not os.path.isfile(ca_certs):
+        raise getmailConfigurationError(
+            'optional ca_certs must be path to a valid file'
+        )
+    return ca_certs
 
 #######################################
-if os.name == 'posix' and os.path.isfile(osx_keychain_binary):
-    def keychain_password(user, server, protocol, logger):
-        """Mac OSX: return a keychain password, if it exists.  Otherwise, return
-        None.
-        """
-        # wish we could pass along a comment to this thing for the user prompt
-        cmd = "%s find-internet-password -g -a '%s' -s '%s' -r '%s'" % (
-            osx_keychain_binary, user, server, protocol
+def check_ssl_version(conf):
+    ssl_version = conf['ssl_version']
+    if ssl_version is None:
+        return None
+    if ssl is None:
+        raise getmailConfigurationError(
+            'specifying ssl_version not supported by this installation of '
+            'Python; requires Python 2.6'
+        )
+    ssl_version = ssl_version.lower()
+    if ssl_version == 'sslv23':
+        return ssl.PROTOCOL_SSLv23
+    elif ssl_version == 'sslv3':
+        return ssl.PROTOCOL_SSLv3
+    elif ssl_version == 'tlsv1':
+        return ssl.PROTOCOL_TLSv1
+    elif ssl_version == 'tlsv1_1' and 'PROTOCOL_TLSv1_1' in dir(ssl):
+        return ssl.PROTOCOL_TLSv1_1
+    elif ssl_version == 'tlsv1_2' and 'PROTOCOL_TLSv1_2' in dir(ssl):
+        return ssl.PROTOCOL_TLSv1_2
+    else:
+        raise getmailConfigurationError(
+            'unknown or unsupported ssl_version'
         )
-        (status, output) = commands.getstatusoutput(cmd)
-        if status != os.EX_OK or not output:
-            logger.error('keychain command %s failed: %s %s' 
-                         % (cmd, status, output))
+
+#######################################
+def check_ssl_fingerprints(conf):
+    ssl_fingerprints = conf['ssl_fingerprints']
+    if ssl_fingerprints is ():
+        return ()
+    if ssl is None or hashlib is None:
+        raise getmailConfigurationError(
+            'specifying ssl_fingerprints not supported by this installation of '
+            'Python; requires Python 2.6'
+        )
+
+    normalized_fprs = []
+    for fpr in ssl_fingerprints:
+        fpr = fpr.lower().replace(':','')
+        if len(fpr) != 64:
+            raise getmailConfigurationError(
+                'ssl_fingerprints must each be the SHA256 certificate hash in hex (with or without colons)'
+            )
+        normalized_fprs.append(fpr)
+    return normalized_fprs
+
+#######################################
+def check_ssl_ciphers(conf):
+    ssl_ciphers = conf['ssl_ciphers']
+    if ssl_ciphers:
+        if sys.version_info < (2, 7, 0):
+            raise getmailConfigurationError(
+                'specifying ssl_ciphers not supported by this installation of '
+                'Python; requires Python 2.7'
+            )
+        if re.search(r'[^a-zA-z0-9, :!\-+@]', ssl_ciphers):
+            raise getmailConfigurationError(
+                'invalid character in ssl_ciphers'
+            )
+    return ssl_ciphers
+
+#######################################
+keychain_password = None
+if os.name == 'posix':
+    if os.path.isfile(osx_keychain_binary):
+        def keychain_password(user, server, protocol, logger):
+            """Mac OSX: return a keychain password, if it exists.  Otherwise, return
+         
+         None.
+            """
+            # OSX protocol is not an arbitrary string; it's a code limited to 
+            # 4 case-sensitive chars, and only specific values.
+            protocol = protocol.lower()
+            if 'imap' in protocol:
+                protocol = 'imap'
+            elif 'pop' in protocol:
+                protocol = 'pop3'
+            else:
+                # This will break.
+                protocol = '????'
+            
+            # wish we could pass along a comment to this thing for the user prompt
+            cmd = "%s find-internet-password -g -a '%s' -s '%s' -r '%s'" % (
+                osx_keychain_binary, user, server, protocol
+            )
+            (status, output) = commands.getstatusoutput(cmd)
+            if status != os.EX_OK or not output:
+                logger.error('keychain command %s failed: %s %s' 
+                             % (cmd, status, output))
+                return None
+            password = None
+            for line in output.split('\n'):
+                match = re.match(r'password: "([^"]+)"', line)
+                if match:
+                    password = match.group(1)
+            if password is None:
+                logger.debug('No keychain password found for %s %s %s'
+                             % (user, server, protocol))
+            return password
+    elif gnomekeyring:
+        def keychain_password(user, server, protocol, logger):
+            """Gnome: return a keyring password, if it exists.  Otherwise, return
+            None.
+            """
+            #logger.trace('trying Gnome keyring for user="%s", server="%s", protocol="%s"\n'
+            #             % (user, server, protocol))
+            try:
+                # http://developer.gnome.org/gnome-keyring/3.5/gnome-keyring
+                # -Network-Passwords.html#gnome-keyring-find-network-password-sync
+                secret = gnomekeyring.find_network_password_sync(
+                    # user, domain=None, server, object=None, protocol,
+                    # authtype=None, port=0
+                    user, None, server, None, protocol, None, 0
+                )
+                
+                #logger.trace('got keyring result %s' % str(secret))
+            except gnomekeyring.NoMatchError:
+                logger.debug('gnome-keyring does not know password for %s %s %s'
+                             % (user, server, protocol))
+                return None
+
+            # secret looks like this:
+            # [{'protocol': 'imap', 'keyring': 'Default', 'server': 'gmail.com', 
+            #   'user': 'hiciu', 'item_id': 1L, 'password': 'kielbasa'}]
+            if secret and 'password' in secret[0]:
+                return secret[0]['password']
+
             return None
-        password = None
-        for line in output.split('\n'):
-            match = re.match(r'password: "([^"]+)"', line)
-            if match:
-                password = match.group(1)
-        if password is None:
-            logger.debug('No keychain password found for %s %s %s'
-                         % (user, server, protocol))
-        return password
-else:
+    #else:
+        # Posix but no OSX keychain or Gnome keyring.
+        # Fallthrough
+if keychain_password is None:
     def keychain_password(user, server, protocol, logger):
-        """Not Mac OSX: always return None.
+        """Neither Mac OSX keychain or Gnome keyring available: always return 
+        None.
         """
         return None
 
 
 #######################################
 def get_password(label, user, server, protocol, logger):
-    # try keychain first
+    # try keychain/keyrings first, where available
     password = keychain_password(user, server, protocol, logger)
-    # if no password found (or not on OSX), prompt in the usual way
-    if not password:
+    if password:
+        logger.debug('using password from keychain/keyring')
+    else:
+        # no password found (or not on OSX), prompt in the usual way
         password = getpass.getpass('Enter password for %s:  ' % label)
     return password
diff -ru getmail-4.32.0/getmail.spec getmail-4.46.0/getmail.spec
--- getmail-4.32.0/getmail.spec	2012-07-06 17:00:39.000000000 -0300
+++ getmail-4.46.0/getmail.spec	2014-04-07 00:25:56.000000000 -0300
@@ -2,7 +2,7 @@
 
 Summary: POP3 mail retriever with reliable Maildir delivery
 Name: getmail
-Version: 4.32.0
+Version: 4.46.0
 Release: 1
 License: GPL
 Group: Applications/Internet
@@ -52,6 +52,105 @@
 %{python_sitelib}/getmailcore/
 
 %changelog
+* Sun Apr 06 2014 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.46.0
+
+* Sun Apr 06 2014 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.46.0
+
+* Sun Mar 30 2014 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.45.0
+
+* Sun Mar 30 2014 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.45.0
+
+* Sun Mar 30 2014 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.44.0
+
+* Sun Mar 30 2014 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.44.0
+
+* Sun Mar 30 2014 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.44.0
+
+* Sat Mar 22 2014 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.44.0
+
+* Sat Mar 22 2014 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.44.0
+
+* Sun Aug 25 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.43.0
+
+* Sun Aug 25 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.43.0
+
+* Sat Aug 03 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.42.0
+
+* Sat Aug 03 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.42.0
+
+* Sat Aug 03 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.42.0
+
+* Sat Aug 03 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.42.0
+
+* Sat Aug 03 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.42.0
+
+* Sun May 26 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.41.0
+
+* Fri May 10 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.40.3
+
+* Wed May 08 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.40.2
+
+* Mon Apr 22 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.40.1
+
+* Sun Apr 21 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.40.0
+
+* Sun Mar 10 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.39.1
+
+* Fri Feb 22 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.39.0
+
+* Fri Feb 22 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.39.0
+
+* Fri Feb 22 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.39.0
+
+* Sat Feb 16 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.38.0
+
+* Sat Feb 16 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.38.0
+
+* Sun Jan 27 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.37.0
+
+* Sun Jan 27 2013 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.37.0
+
+* Sat Dec 15 2012 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.36.0
+
+* Wed Oct 24 2012 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.35.0
+
+* Sat Sep 08 2012 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.34.0
+
+* Tue Aug 07 2012 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
+-update to version 4.33.0
+
 * Fri Jul 06 2012 Charles Cazabon <charlesc-getmail-...@pyropus.ca>
 -update to version 4.32.0
 
diff -ru getmail-4.32.0/PKG-INFO getmail-4.46.0/PKG-INFO

Reply via email to