diff --git a/offlineimap.conf b/offlineimap.conf index 3ffc2e6..1b3214a 100644 --- a/offlineimap.conf +++ b/offlineimap.conf @@ -380,6 +380,15 @@ remoterepository = RemoteExample #filterheaders = X-Some-Weird-Header +# This option stands in the [Account Test] section. +# +# Use proxy connection for this account. Usefull to bypass the GFW in China. +# To specify a proxy connection, join proxy type, host and port with colons. +# Available proxy types are SOCKS5, SOCKS4, HTTP. +# You also need to install PySocks through pip. +# +#proxy = SOCKS5:localhost:9999 + [Repository LocalExample] # Each repository requires a "type" declaration. The types supported for diff --git a/offlineimap/imaplibutil.py b/offlineimap/imaplibutil.py index 83ffe9a..47328bf 100644 --- a/offlineimap/imaplibutil.py +++ b/offlineimap/imaplibutil.py @@ -21,6 +21,7 @@ import subprocess from sys import exc_info import threading from hashlib import sha1 +import socket from offlineimap.ui import getglobalui from offlineimap import OfflineImapError @@ -69,6 +70,37 @@ class UsefulIMAPMixIn(object): def _mesg(self, s, tn=None, secs=None): new_mesg(self, s, tn, secs) + # Overrides private function from IMAP4 (@imaplib2) + def open_socket(self): + """open_socket() + Open socket choosing first address family available.""" + msg = (-1, 'could not open socket') + for res in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + try: + # use socket of our own, possiblly socksified socket. + s = self.socket(af, socktype, proto) + except socket.error, msg: + continue + try: + for i in (0, 1): + try: + s.connect(sa) + break + except socket.error, msg: + if len(msg.args) < 2 or msg.args[0] != errno.EINTR: + raise + else: + raise socket.error(msg) + except socket.error, msg: + s.close() + continue + break + else: + raise socket.error(msg) + + return s + class IMAP4_Tunnel(UsefulIMAPMixIn, IMAP4): """IMAP4 client class over a tunnel @@ -79,6 +111,9 @@ class IMAP4_Tunnel(UsefulIMAPMixIn, IMAP4): The result will be in PREAUTH stage.""" def __init__(self, tunnelcmd, **kwargs): + if "use_socket" in kwargs: + self.socket = kwargs['use_socket'] + del kwargs['use_socket'] IMAP4.__init__(self, tunnelcmd, **kwargs) def open(self, host, port): @@ -141,6 +176,9 @@ class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL): """Improved version of imaplib.IMAP4_SSL overriding select().""" def __init__(self, *args, **kwargs): + if "use_socket" in kwargs: + self.socket = kwargs['use_socket'] + del kwargs['use_socket'] self._fingerprint = kwargs.get('fingerprint', None) if type(self._fingerprint) != type([]): self._fingerprint = [self._fingerprint] @@ -171,7 +209,11 @@ class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL): class WrappedIMAP4(UsefulIMAPMixIn, IMAP4): """Improved version of imaplib.IMAP4 overriding select().""" - pass + def __init__(self, *args, **kwargs): + if "use_socket" in kwargs: + self.socket = kwargs['use_socket'] + del kwargs['use_socket'] + IMAP4.__init__(self, *args, **kwargs) def Internaldate2epoch(resp): diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index 3d69426..351ee9f 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -15,10 +15,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -from offlineimap import imaplibutil, imaputil, threadutil, OfflineImapError -from offlineimap.ui import getglobalui from threading import Lock, BoundedSemaphore, Thread, Event, currentThread -import offlineimap.accounts import hmac import socket import base64 @@ -28,6 +25,11 @@ from sys import exc_info from socket import gaierror from ssl import SSLError, cert_time_to_seconds +from offlineimap import imaplibutil, imaputil, threadutil, OfflineImapError +import offlineimap.accounts +from offlineimap.ui import getglobalui + + try: # do we have a recent pykerberos? have_gss = False @@ -101,6 +103,31 @@ class IMAPServer: self.gss_vc = None self.gssapi = False + # In order to support proxy connection, we have to override the + # default socket instance with our own socksified socket instance. + # We add this option to bypass the GFW in China. + _account_section = 'Account ' + self.repos.account.name + if not self.config.has_option(_account_section, 'proxy'): + self.proxied_socket = socket.socket + else: + proxy = self.config.get(_account_section, 'proxy') + # Powered by PySocks. + try: + import socks + proxy_type, host, port = proxy.split(":") + port = int(port) + socks.setdefaultproxy(getattr(socks, proxy_type), host, port) + self.proxied_socket = socks.socksocket + except ImportError: + self.ui.warn("PySocks not installed, ignoring proxy option.") + self.proxied_socket = socket.socket + except (AttributeError, ValueError) as e: + self.ui.warn("Bad proxy option %s for account %s: %s " + "Ignoring proxy option."% + (proxy, self.repos.account.name, e)) + self.proxied_socket = socket.socket + + def __getpassword(self): """Returns the server password or None""" if self.goodpassword != None: # use cached good one first @@ -391,25 +418,33 @@ class IMAPServer: # Generate a new connection. if self.tunnel: self.ui.connecting('tunnel', self.tunnel) - imapobj = imaplibutil.IMAP4_Tunnel(self.tunnel, - timeout=socket.getdefaulttimeout()) + imapobj = imaplibutil.IMAP4_Tunnel( + self.tunnel, + timeout=socket.getdefaulttimeout(), + use_socket=self.proxied_socket, + ) success = 1 elif self.usessl: self.ui.connecting(self.hostname, self.port) - imapobj = imaplibutil.WrappedIMAP4_SSL(self.hostname, - self.port, - self.sslclientkey, - self.sslclientcert, - self.sslcacertfile, - self.__verifycert, - self.sslversion, - timeout=socket.getdefaulttimeout(), - fingerprint=self.fingerprint - ) + imapobj = imaplibutil.WrappedIMAP4_SSL( + self.hostname, + self.port, + self.sslclientkey, + self.sslclientcert, + self.sslcacertfile, + self.__verifycert, + self.sslversion, + timeout=socket.getdefaulttimeout(), + fingerprint=self.fingerprint, + use_socket=self.proxied_socket, + ) else: self.ui.connecting(self.hostname, self.port) - imapobj = imaplibutil.WrappedIMAP4(self.hostname, self.port, - timeout=socket.getdefaulttimeout()) + imapobj = imaplibutil.WrappedIMAP4( + self.hostname, self.port, + timeout=socket.getdefaulttimeout(), + use_socket=self.proxied_socket, + ) if not self.preauth_tunnel: try: