Configurable thread status character for ui.Curses.Blinkenlights

This commit is contained in:
aaron 2006-12-11 06:12:00 +01:00
parent e16881ca2a
commit 71c8b2e7c4
27 changed files with 2220 additions and 623 deletions

View File

@ -1,5 +1,5 @@
offlineimap Mail syncing software
Copyright (C) 2002 - 2007 John Goerzen
Copyright (C) 2002 - 2006 John Goerzen
<jgoerzen@complete.org>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -15,3 +15,57 @@ Copyright (C) 2002 - 2007 John Goerzen
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
ONLY imaplib.py is Copyright (c) 2001 Python Software Foundation;
All Rights Reserved
imaplib.py comes from Python dev tree and is licensed under the
GPL-compatible PSF license as follows:
PSF LICENSE AGREEMENT FOR PYTHON 2.2
------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using Python 2.2 software in source or binary form and its
associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 2.2
alone or in any derivative version, provided, however, that PSF's
License Agreement and PSF's notice of copyright, i.e., "Copyright (c)
2001 Python Software Foundation; All Rights Reserved" are retained in
Python 2.2 alone or in any derivative version prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 2.2 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 2.2.
4. PSF is making Python 2.2 available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.2 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
2.2 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.2,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python 2.2, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.

View File

@ -33,7 +33,7 @@ clean:
-find . -name auth -exec rm -vf {}/password {}/username \;
-rm -f manual.html manual.pdf manual.txt offlineimap.1
doc:
doc: faq
docbook2man offlineimap.sgml
docbook2man offlineimap.sgml
docbook2html -u offlineimap.sgml

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python
# Startup from system-wide installation
# Copyright (C) 2002 - 2007 John Goerzen
# Copyright (C) 2002 - 2006 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
@ -18,4 +18,4 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from offlineimap import init
init.startup('5.99.0')
init.startup('4.0.16')

24
debian/changelog vendored
View File

@ -1,27 +1,3 @@
offlineimap (5.99.0) unstable; urgency=low
* Re-scan remote folder names at the start of each sync run.
Closes: #329000, #396772.
* Drop internal imaplib.py in favor of default Python one.
* New user interface: Machine.MachineUI. Machine-parsable.
* Drop all the Tk interfaces.
Closes: #265088.
* Now supports specifying timeouts for socket operations.
* Updated copyright files.
* Improved interaction with Dovecot maildirs. Patch thanks to Asheesh
Laroia.
* Improved filesystem syncing semantics, which should reduce duplication
in the event of hardware failure.
* UID validity diagnostics improvement. Patch from David Favro.
* No longer leave preauthtunnel zombies with autorefresh.
Patch from Peter Colberg. Closes: #410730.
* --help now shows available UIs. Patch from Daniel Rall.
* Check all resolved addresses. Patch from Mark Brown. Closes: #413030.
* Removed todo directory from tree, moved to BTS.
* New PID file to enable third-party "kill offlineimap" tools.
-- John Goerzen <jgoerzen@complete.org> Tue, 10 Jul 2007 04:10:26 -0500
offlineimap (4.0.16) unstable; urgency=low
* Apply patches from Danial Burrows to improve situation when

1
debian/control vendored
View File

@ -10,6 +10,7 @@ Standards-Version: 3.7.2
Package: offlineimap
Architecture: all
Depends: ${python:Depends}
Suggests: python-tk
Description: IMAP/Maildir synchronization and reader support
OfflineIMAP is a tool to simplify your e-mail reading. With
OfflineIMAP, you can:

2
debian/copyright vendored
View File

@ -4,7 +4,7 @@ on Fri, 21 Jun 2002 14:54:56 -0500.
The original source can always be found at:
http://software.complete.org/offlineimap/
Copyright (C) 2002 - 2007 John Goerzen
Copyright (C) 2002 - 2006 John Goerzen
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -51,18 +51,18 @@ maxsyncaccounts = 1
# fails, the second, and so forth.
#
# The pre-defined options are:
# Tk.Blinkenlights -- A graphical interface, shows LEDs and a single log
# Tk.VerboseUI -- A graphical interface, shows logs per thread
# Curses.Blinkenlights -- A text-based (terminal) interface similar to
# Tk.Blinkenlights
# TTY.TTYUI -- a text-based (terminal) interface
# Noninteractive.Basic -- Noninteractive interface suitable for cronning
# Noninteractive.Quiet -- Noninteractive interface, generates no output
# except for errors.
# Machine.MachineUI -- Interactive interface suitable for machine
# parsing.
#
# You can override this with a command-line option -u.
ui = Curses.Blinkenlights, TTY.TTYUI,
ui = Tk.Blinkenlights, Tk.VerboseUI, Curses.Blinkenlights, TTY.TTYUI,
Noninteractive.Basic, Noninteractive.Quiet
# If you try to synchronize messages to a read-only folder,
@ -85,19 +85,6 @@ ignore-readonly = no
# pythonfile = ~/.offlineimap.py
#
# By default, OfflineIMAP will not exit due to a network error until
# the operating system returns an error code. Operating systems can sometimes
# take forever to notice this. Here you can activate a timeout on the
# socket. This timeout applies to individual socket reads and writes,
# not to an overall sync operation. You could perfectly well have a 30s
# timeout here and your sync still take minutes.
#
# Values in the 30-120 second range are reasonable.
#
# The default is to have no timeout beyond the OS. Times are given in seconds.
#
# socktimeout = 60
##################################################
# Mailbox name recorder
##################################################
@ -129,6 +116,37 @@ footer = "\n"
# Note that this filter can be used only to further restrict mbnames
# to a subset of folders that pass the account's folderfilter.
##################################################
# Blinkenlights configuration
##################################################
[ui.Tk.Blinkenlights]
# Specifies the default number of lines in the log.
loglines = 5
# Specifies how many lines are in the scrollback log buffer.
bufferlines = 500
# If true, says that the log should be enabled by default.
# Otherwise, you have to click "Show Log" to enable the log.
showlog = false
# Sets the font information.
fontfamily = Helvetica
fontsize = 8
[ui.Curses.Blinkenlights]
# Character used to indicate thread status, in place of
# the LEDs from Tk.Blinkenlights
statuschar = .
##################################################
# Accounts
##################################################

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python
# Startup from single-user installation
# Copyright (C) 2002 - 2007 John Goerzen
# Copyright (C) 2002 - 2006 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
@ -18,4 +18,4 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from offlineimap import init
init.startup('5.99.0')
init.startup('4.0.16')

View File

@ -208,13 +208,14 @@ remoteuser = jgoerzen
</listitem>
<listitem>
<para>
You must have Python version 2.4 or above installed.
You must have Python version 2.2.1 or above installed.
If you are
running on Debian GNU/Linux, this requirement will automatically be
taken care of for you. If you do not have Python already, check with
your system administrator or operating system vendor; or, download it from
<ulink url="http://www.python.org/">the Python website</ulink>.
If you intend to use the SSL interface, your
If you intend to use the Tk interface, you must have Tkinter
(python-tk) installed. If you intend to use the SSL interface, your
Python must have been built with SSL support.
</para>
</listitem>
@ -411,22 +412,27 @@ cd offlineimap-x.y.z</ProgramListing>
option can override the configuration file setting. The available
values for the configuration file or command-line are described
in this section.</para>
<refsect2>
<title>Curses.Blinkenlights</title>
<para>
Curses.Blinkenlights is an interface designed to be sleek, fun to watch, and
<title>Tk.Blinkenlights</title>
<para>Tk.Blinkenlights is an interface designed to be sleek, fun to watch, and
informative of the overall picture of what &OfflineIMAP;
is doing. I consider it to be the best general-purpose interface in
&OfflineIMAP;.
</para>
<para>
Curses.Blinkenlights contains a row of
"LEDs" with command buttons and a log.
The log shows more
Tk.Blinkenlights contains, by default, a small window with a row of
LEDs, a small log, and a row of command buttons.
The total size of the window is
very small, so it uses little desktop space, yet it is quite
functional. The optional, toggleable, log shows more
detail about what is happening and is color-coded to match the color
of the lights.
</para>
<para>
Tk.Blinkenlights is the only user interface that has configurable
parameters; see the example <filename>offlineimap.conf</filename>
for more details.
</para>
<para>
Each light in the Blinkenlights interface represents a thread
of execution -- that is, a particular task that &OfflineIMAP;
@ -532,6 +538,32 @@ cd offlineimap-x.y.z</ProgramListing>
</blockquote>
</refsect2>
<refsect2>
<title>Curses.Blinkenlights</title>
<para>
Curses.Blinkenlights is an interface very similar to Tk.Blinkenlights,
but is designed to be run in a console window (an xterm, Linux virtual
terminal, etc.) Since it doesn't have access to graphics, it isn't
quite as pretty, but it still gets the job done.
</para>
<para>Please see the Tk.Blinkenlights section above for more
information about the colors used in this interface.
</para>
</refsect2>
<refsect2>
<title>Tk.VerboseUI</title>
<para>
Tk.VerboseUI (formerly known as Tk.TkUI) is a graphical interface
that presents a variable-sized window. In the window, each
currently-executing thread has a section where its name and current
status are displayed. This interface is best suited to people running
on slower connections, as you get a lot of detail, but for fast
connections, the detail may go by too quickly to be useful. People
with fast connections may wish to use Tk.Blinkenlights instead.
</para>
</refsect2>
<refsect2>
<title>TTY.TTYUI</title>
<para>
@ -566,15 +598,6 @@ cd offlineimap-x.y.z</ProgramListing>
</para>
</refsect2>
<refsect2>
<title>Machine.MachineUI</title>
<para>
Machine.MachineUI generates output in a machine-parsable format.
It is designed for other programs that will interface
to OfflineIMAP.
</para>
</refsect2>
</refsect1>
<refsect1>
@ -708,24 +731,6 @@ def test_mycmp():
</para>
</refsect2>
</refsect1>
<refsect1>
<title>Signals</title>
<para>
OfflineIMAP writes its current PID into
<filename>~/.offlineimap/pid</filename> when it is
running. It is not guaranteed that this file will
not exist when OfflineIMAP is not running.
</para>
<!-- not done yet
<para>
You can send SIGINT to OfflineIMAP using this file to
kill it. SIGUSR1 will force an immediate resync of
all accounts. This will be ignored for all accounts
for which a resync is already in progress.
</para>
-->
</refsect1>
<refsect1>
<title>Errors</title>
@ -831,8 +836,8 @@ rm -r ~/.offlineimap/Repository-<replaceable>RepositoryName</></programlisting>
<para>&OfflineIMAP; is not designed to have several instances (for instance, a cron job and an interactive invocation) run over the same
mailbox simultaneously. It will perform a check on startup and
abort if another &OfflineIMAP; is already running. If you need
to schedule synchronizations, you'll probably find
<property>autorefresh</property> settings more convenient than cron.
to schedule synchronizations, please use the
<property>autorefresh</property> settings rather than cron.
Alternatively, you can set a separate <property>metadata</property>
directory for each instance.
</para>

View File

@ -1,6 +1,8 @@
# Copyright (C) 2003 John Goerzen
# <jgoerzen@complete.org>
#
# Portions Copyright (C) 2007 David Favro <offlineimap@meta-dynamic.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
@ -146,8 +148,6 @@ class AccountSynchronizationMixin:
folderthreads.append(thread)
threadutil.threadsreset(folderthreads)
mbnames.write()
localrepos.forgetfolders()
remoterepos.forgetfolders()
localrepos.holdordropconnections()
remoterepos.holdordropconnections()
finally:

View File

@ -1,5 +1,5 @@
# IMAP folder support
# Copyright (C) 2002-2007 John Goerzen
# Copyright (C) 2002-2004 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
@ -17,8 +17,7 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from Base import BaseFolder
import imaplib
from offlineimap import imaputil, imaplibutil
from offlineimap import imaputil, imaplib
from offlineimap.ui import UIBase
from offlineimap.version import versionstr
import rfc822, time, string, random, binascii, re
@ -99,7 +98,7 @@ class IMAPFolder(BaseFolder):
else:
uid = long(options['UID'])
flags = imaputil.flagsimap2maildir(options['FLAGS'])
rtime = imaplibutil.Internaldate2epoch(messagestr)
rtime = imaplib.Internaldate2epoch(messagestr)
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime}
def getmessagelist(self):
@ -271,7 +270,7 @@ class IMAPFolder(BaseFolder):
return
result = imapobj.uid('store', '%d' % uid, 'FLAGS',
imaputil.flagsmaildir2imap(flags))
assert result[0] == 'OK', 'Error with store: ' + '. '.join(r[1])
assert result[0] == 'OK', 'Error with store: ' + r[1]
finally:
self.imapserver.releaseconnection(imapobj)
result = result[1][0]
@ -317,7 +316,7 @@ class IMAPFolder(BaseFolder):
imaputil.listjoin(uidlist),
operation + 'FLAGS',
imaputil.flagsmaildir2imap(flags))
assert r[0] == 'OK', 'Error with store: ' + '. '.join(r[1])
assert r[0] == 'OK', 'Error with store: ' + r[1]
r = r[1]
finally:
self.imapserver.releaseconnection(imapobj)

View File

@ -1,5 +1,5 @@
# Local status cache virtual folder
# Copyright (C) 2002 - 2007 John Goerzen
# Copyright (C) 2002 - 2003 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
@ -90,18 +90,8 @@ class LocalStatusFolder(BaseFolder):
flags.sort()
flags = ''.join(flags)
file.write("%s:%s\n" % (msg['uid'], flags))
file.flush()
os.fsync(file.fileno())
file.close()
os.rename(self.filename + ".tmp", self.filename)
try:
fd = os.open(os.path.dirname(self.filename), os.O_RDONLY)
os.fsync(fd)
os.close(fd)
except:
pass
finally:
self.savelock.release()

View File

@ -1,5 +1,5 @@
# Maildir folder support
# Copyright (C) 2002 - 2007 John Goerzen
# Copyright (C) 2002 - 2006 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
@ -130,8 +130,6 @@ class MaildirFolder(BaseFolder):
return st.st_mtime
def savemessage(self, uid, content, flags, rtime):
# This function only ever saves to tmp/,
# but it calls savemessageflags() to actually save to cur/ or new/.
ui = UIBase.getglobalui()
ui.debug('maildir', 'savemessage: called to write with flags %s and content %s' % \
(repr(flags), repr(content)))
@ -142,9 +140,12 @@ class MaildirFolder(BaseFolder):
# We already have it.
self.savemessageflags(uid, flags)
return uid
# Otherwise, save the message in tmp/ and then call savemessageflags()
# to give it a permanent home.
if 'S' in flags:
# If a message has been seen, it goes into the cur
# directory. CR debian#152482, [complete.org #4]
newdir = os.path.join(self.getfullname(), 'cur')
else:
newdir = os.path.join(self.getfullname(), 'new')
tmpdir = os.path.join(self.getfullname(), 'tmp')
messagename = None
attempts = 0
@ -168,31 +169,16 @@ class MaildirFolder(BaseFolder):
ui.debug('maildir', 'savemessage: using temporary name %s' % tmpmessagename)
file = open(os.path.join(tmpdir, tmpmessagename), "wt")
file.write(content)
# Make sure the data hits the disk
file.flush()
os.fsync(file.fileno())
file.close()
if rtime != None:
os.utime(os.path.join(tmpdir,tmpmessagename), (rtime,rtime))
ui.debug('maildir', 'savemessage: moving from %s to %s' % \
(tmpmessagename, messagename))
if tmpmessagename != messagename: # then rename it
os.link(os.path.join(tmpdir, tmpmessagename),
os.path.join(tmpdir, messagename))
os.unlink(os.path.join(tmpdir, tmpmessagename))
try:
# fsync the directory (safer semantics in Linux)
fd = os.open(tmpdir, os.O_RDONLY)
os.fsync(fd)
os.close(fd)
except:
pass
os.link(os.path.join(tmpdir, tmpmessagename),
os.path.join(newdir, messagename))
os.unlink(os.path.join(tmpdir, tmpmessagename))
self.messagelist[uid] = {'uid': uid, 'flags': [],
'filename': os.path.join(tmpdir, messagename)}
'filename': os.path.join(newdir, messagename)}
self.savemessageflags(uid, flags)
ui.debug('maildir', 'savemessage: returning uid %d' % uid)
return uid
@ -203,7 +189,6 @@ class MaildirFolder(BaseFolder):
def savemessageflags(self, uid, flags):
oldfilename = self.messagelist[uid]['filename']
newpath, newname = os.path.split(oldfilename)
tmpdir = os.path.join(self.getfullname(), 'tmp')
if 'S' in flags:
# If a message has been seen, it goes into the cur
# directory. CR debian#152482, [complete.org #4]
@ -226,10 +211,6 @@ class MaildirFolder(BaseFolder):
self.messagelist[uid]['flags'] = flags
self.messagelist[uid]['filename'] = newfilename
# By now, the message had better not be in tmp/ land!
final_dir, final_name = os.path.split(self.messagelist[uid]['filename'])
assert final_dir != tmpdir
def deletemessage(self, uid):
if not uid in self.messagelist:
return

1453
offlineimap/imaplib.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,199 +0,0 @@
# imaplib utilities
# Copyright (C) 2002-2007 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import re, string, types, binascii, socket, time, random, subprocess, sys, os
from offlineimap.ui import UIBase
from imaplib import *
# Import the symbols we need that aren't exported by default
from imaplib import IMAP4_PORT, IMAP4_SSL_PORT, InternalDate, Mon2num
class IMAP4_Tunnel(IMAP4):
"""IMAP4 client class over a tunnel
Instantiate with: IMAP4_Tunnel(tunnelcmd)
tunnelcmd -- shell command to generate the tunnel.
The result will be in PREAUTH stage."""
def __init__(self, tunnelcmd):
IMAP4.__init__(self, tunnelcmd)
def open(self, host, port):
"""The tunnelcmd comes in on host!"""
self.process = subprocess.Popen(host, shell=True, close_fds=True,
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
(self.outfd, self.infd) = (self.process.stdin, self.process.stdout)
def read(self, size):
retval = ''
while len(retval) < size:
retval += self.infd.read(size - len(retval))
return retval
def readline(self):
return self.infd.readline()
def send(self, data):
self.outfd.write(data)
def shutdown(self):
self.infd.close()
self.outfd.close()
self.process.wait()
class sslwrapper:
def __init__(self, sslsock):
self.sslsock = sslsock
self.readbuf = ''
def write(self, s):
return self.sslsock.write(s)
def _read(self, n):
return self.sslsock.read(n)
def read(self, n):
if len(self.readbuf):
# Return the stuff in readbuf, even if less than n.
# It might contain the rest of the line, and if we try to
# read more, might block waiting for data that is not
# coming to arrive.
bytesfrombuf = min(n, len(self.readbuf))
retval = self.readbuf[:bytesfrombuf]
self.readbuf = self.readbuf[bytesfrombuf:]
return retval
retval = self._read(n)
if len(retval) > n:
self.readbuf = retval[n:]
return retval[:n]
return retval
def readline(self):
retval = ''
while 1:
linebuf = self.read(1024)
nlindex = linebuf.find("\n")
if nlindex != -1:
retval += linebuf[:nlindex + 1]
self.readbuf = linebuf[nlindex + 1:] + self.readbuf
return retval
else:
retval += linebuf
def new_mesg(self, s, secs=None):
if secs is None:
secs = time.time()
tm = time.strftime('%M:%S', time.localtime(secs))
UIBase.getglobalui().debug('imap', ' %s.%02d %s' % (tm, (secs*100)%100, s))
def new_open(self, host = '', port = IMAP4_PORT):
"""Setup connection to remote server on "host:port"
(default: localhost:standard IMAP4 port).
This connection will be used by the routines:
read, readline, send, shutdown.
"""
self.host = host
self.port = port
res = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM)
self.sock = socket.socket(af, socktype, proto)
# Try each address returned by getaddrinfo in turn until we
# manage to connect to one.
# Try all the addresses in turn until we connect()
last_error = 0
for remote in res:
af, socktype, proto, canonname, sa = remote
self.sock = socket.socket(af, socktype, proto)
last_error = self.sock.connect_ex(sa)
if last_error == 0:
break
else:
self.sock.close()
if last_error != 0:
# FIXME
raise socket.error(last_error)
self.file = self.sock.makefile('rb')
def new_open_ssl(self, host = '', port = IMAP4_SSL_PORT):
"""Setup connection to remote server on "host:port".
(default: localhost:standard IMAP4 SSL port).
This connection will be used by the routines:
read, readline, send, shutdown.
"""
self.host = host
self.port = port
#This connects to the first ip found ipv4/ipv6
#Added by Adriaan Peeters <apeeters@lashout.net> based on a socket
#example from the python documentation:
#http://www.python.org/doc/lib/socket-example.html
res = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM)
# Try all the addresses in turn until we connect()
last_error = 0
for remote in res:
af, socktype, proto, canonname, sa = remote
self.sock = socket.socket(af, socktype, proto)
last_error = self.sock.connect_ex(sa)
if last_error == 0:
break
else:
self.sock.close()
if last_error != 0:
# FIXME
raise socket.error(last_error)
if sys.version_info[0] <= 2 and sys.version_info[1] <= 2:
self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile)
else:
self.sslobj = socket.ssl(self.sock._sock, self.keyfile, self.certfile)
self.sslobj = sslwrapper(self.sslobj)
mustquote = re.compile(r"[^\w!#$%&'+,.:;<=>?^`|~-]")
def Internaldate2epoch(resp):
"""Convert IMAP4 INTERNALDATE to UT.
Returns seconds since the epoch.
"""
mo = InternalDate.match(resp)
if not mo:
return None
mon = Mon2num[mo.group('mon')]
zonen = mo.group('zonen')
day = int(mo.group('day'))
year = int(mo.group('year'))
hour = int(mo.group('hour'))
min = int(mo.group('min'))
sec = int(mo.group('sec'))
zoneh = int(mo.group('zoneh'))
zonem = int(mo.group('zonem'))
# INTERNALDATE timezone must be subtracted to get UT
zone = (zoneh*60 + zonem)*60
if zonen == '-':
zone = -zone
tt = (year, mon, day, hour, min, sec, -1, -1, -1)
return time.mktime(tt)

View File

@ -1,5 +1,5 @@
# IMAP server support
# Copyright (C) 2002 - 2007 John Goerzen
# Copyright (C) 2002, 2003 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
@ -16,8 +16,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import imaplib
from offlineimap import imaplibutil, imaputil, threadutil
from offlineimap import imaplib, imaputil, threadutil
from offlineimap.ui import UIBase
from threading import *
import thread, hmac, os
@ -32,8 +31,8 @@ class UsefulIMAPMixIn:
return None
def select(self, mailbox='INBOX', readonly=None, force = 0):
if (not force) and self.getselectedfolder() == mailbox \
and self.is_readonly == readonly:
if (not force) and self.getselectedfolder() == mailbox:
self.is_readonly = readonly
# No change; return.
return
result = self.__class__.__bases__[1].select(self, mailbox, readonly)
@ -44,18 +43,9 @@ class UsefulIMAPMixIn:
else:
self.selectedfolder = None
def _mesg(self, s, secs=None):
imaplibutil.new_mesg(self, s, secs)
class UsefulIMAP4(UsefulIMAPMixIn, imaplib.IMAP4):
def open(self, host = '', port = imaplib.IMAP4_PORT):
imaplibutil.new_open(self, host, port)
class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplib.IMAP4_SSL):
def open(self, host = '', port = imaplib.IMAP4_SSL_PORT):
imaplibutil.new_open_ssl(self, host, port)
class UsefulIMAP4_Tunnel(UsefulIMAPMixIn, imaplibutil.IMAP4_Tunnel): pass
class UsefulIMAP4(UsefulIMAPMixIn, imaplib.IMAP4): pass
class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplib.IMAP4_SSL): pass
class UsefulIMAP4_Tunnel(UsefulIMAPMixIn, imaplib.IMAP4_Tunnel): pass
class IMAPServer:
def __init__(self, config, reposname,
@ -67,7 +57,6 @@ class IMAPServer:
self.username = username
self.password = password
self.passworderror = None
self.goodpassword = None
self.hostname = hostname
self.tunnel = tunnel
self.port = port
@ -88,9 +77,6 @@ class IMAPServer:
self.reference = reference
def getpassword(self):
if self.goodpassword != None:
return self.goodpassword
if self.password != None and self.passworderror == None:
return self.password
@ -113,7 +99,6 @@ class IMAPServer:
def releaseconnection(self, connection):
"""Releases a connection, returning it to the pool."""
self.connectionlock.acquire()
self.assignedconnections.remove(connection)
self.availableconnections.append(connection)
@ -182,8 +167,6 @@ class IMAPServer:
UIBase.getglobalui().connecting(self.hostname, self.port)
imapobj = UsefulIMAP4(self.hostname, self.port)
imapobj.mustquote = imaplibutil.mustquote
if not self.tunnel:
try:
if 'AUTH=CRAM-MD5' in imapobj.capabilities:
@ -197,7 +180,6 @@ class IMAPServer:
self.plainauth(imapobj)
# Would bail by here if there was a failure.
success = 1
self.goodpassword = self.password
except imapobj.error, val:
self.passworderror = str(val)
self.password = None

View File

@ -1,5 +1,5 @@
# OfflineIMAP initialization code
# Copyright (C) 2002-2007 John Goerzen
# Copyright (C) 2002, 2003 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
@ -16,15 +16,14 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import imaplib
from offlineimap import imapserver, repository, folder, mbnames, threadutil, version, syncmaster, accounts
from offlineimap import imaplib, imapserver, repository, folder, mbnames, threadutil, version, syncmaster, accounts
from offlineimap.localeval import LocalEval
from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
from offlineimap.ui import UIBase
import re, os, os.path, offlineimap, sys
from offlineimap.CustomConfig import CustomConfigParser
from threading import *
import threading, socket
import threading
from getopt import getopt
try:
@ -50,14 +49,14 @@ def startup(versionno):
assert versionno == version.versionstr, "Revision of main program (%s) does not match that of library (%s). Please double-check your PYTHONPATH and installation locations." % (versionno, version.versionstr)
options = {}
if '--help' in sys.argv[1:]:
sys.stdout.write(version.getcmdhelp() + "\n")
sys.stdout.write(version.cmdhelp + "\n")
sys.exit(0)
for optlist in getopt(sys.argv[1:], 'P:1oa:c:d:l:u:h')[0]:
options[optlist[0]] = optlist[1]
if options.has_key('-h'):
sys.stdout.write(version.getcmdhelp())
sys.stdout.write(version.cmdhelp)
sys.stdout.write("\n")
sys.exit(0)
configfilename = os.path.expanduser("~/.offlineimaprc")
@ -102,21 +101,10 @@ def startup(versionno):
lock(config, ui)
try:
pidfd = open(config.getmetadatadir() + "/pid", "w")
pidfd.write(os.getpid())
pidfd.close()
except:
pass
try:
if options.has_key('-l'):
sys.stderr = ui.logfile
socktimeout = config.getdefaultint("general", "socktimeout", 0)
if socktimeout > 0:
socket.setdefaulttimeout(socktimeout)
activeaccounts = config.get("general", "accounts")
if options.has_key('-a'):
activeaccounts = options['-a']

View File

@ -1,5 +1,5 @@
# Base repository support
# Copyright (C) 2002-2007 John Goerzen
# Copyright (C) 2002, 2003, 2006 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
@ -63,15 +63,6 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
return self.restore_folder_atimes()
def connect(self):
"""Establish a connection to the remote, if necessary. This exists
so that IMAP connections can all be established up front, gathering
passwords as needed. It was added in order to support the
error recovery -- we need to connect first outside of the error
trap in order to validate the password, and that's the point of
this function."""
pass
def holdordropconnections(self):
pass
@ -106,11 +97,6 @@ class BaseRepository(CustomConfig.ConfigHelperMixin):
"""Returns a list of ALL folders on this server."""
return []
def forgetfolders(self):
"""Forgets the cached list of folders, if any. Useful to run
after a sync run."""
pass
def getsep(self):
raise NotImplementedError

View File

@ -85,30 +85,30 @@ class IMAPRepository(BaseRepository):
return self.imapserver.delim
def gethost(self):
host = None
host = None
localeval = self.localeval
if self.config.has_option(self.getsection(), 'remotehosteval'):
host = self.getconf('remotehosteval')
if host != None:
return localeval.eval(host)
host = self.getconf('remotehosteval')
if host != None:
return localeval.eval(host)
host = self.getconf('remotehost')
if host != None:
return host
host = self.getconf('remotehost')
if host != None:
return host
def getuser(self):
user = None
user = None
localeval = self.localeval
if self.config.has_option(self.getsection(), 'remoteusereval'):
user = self.getconf('remoteusereval')
if user != None:
return localeval.eval(user)
user = self.getconf('remoteusereval')
if user != None:
return localeval.eval(user)
user = self.getconf('remoteuser')
if user != None:
return user
user = self.getconf('remoteuser')
if user != None:
return user
def getport(self):
return self.getconfint('remoteport', None)
@ -129,13 +129,13 @@ class IMAPRepository(BaseRepository):
return self.getconfboolean('expunge', 1)
def getpassword(self):
passwd = None
passwd = None
localeval = self.localeval
if self.config.has_option(self.getsection(), 'remotepasseval'):
passwd = self.getconf('remotepasseval')
if passwd != None:
return localeval.eval(passwd)
passwd = self.getconf('remotepasseval')
if passwd != None:
return localeval.eval(passwd)
password = self.getconf('remotepass', None)
if password != None:
@ -145,7 +145,7 @@ class IMAPRepository(BaseRepository):
fd = open(os.path.expanduser(passfile))
password = fd.readline().strip()
fd.close()
return password
return password
return None
def getfolder(self, foldername):
@ -156,13 +156,6 @@ class IMAPRepository(BaseRepository):
def getfoldertype(self):
return folder.IMAP.IMAPFolder
def connect(self):
imapobj = self.imapserver.acquireconnection()
self.imapserver.releaseconnection(imapobj)
def forgetfolders(self):
self.folders = None
def getfolders(self):
if self.folders != None:
return self.folders

View File

@ -1,5 +1,5 @@
# OfflineIMAP synchronization master code
# Copyright (C) 2002-2007 John Goerzen
# Copyright (C) 2002 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
@ -16,8 +16,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import imaplib
from offlineimap import imapserver, repository, folder, mbnames, threadutil, version
from offlineimap import imaplib, imapserver, repository, folder, mbnames, threadutil, version
from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
import offlineimap.accounts
from offlineimap.accounts import SyncableAccount

View File

@ -126,10 +126,11 @@ class CursesUtil:
self.start()
class CursesAccountFrame:
def __init__(s, master, accountname):
def __init__(s, master, accountname, ui):
s.c = master
s.children = []
s.accountname = accountname
s.ui = ui
def drawleadstr(s, secs = None):
if secs == None:
@ -150,7 +151,7 @@ class CursesAccountFrame:
s.location += 1
def getnewthreadframe(s):
tf = CursesThreadFrame(s.c, s.window, 0, s.location)
tf = CursesThreadFrame(s.c, s.ui, s.window, 0, s.location)
s.location += 1
s.children.append(tf)
return tf
@ -180,9 +181,10 @@ class CursesAccountFrame:
s.sleeping_abort = 1
class CursesThreadFrame:
def __init__(s, master, window, y, x):
def __init__(s, master, ui, window, y, x):
"""master should be a CursesUtil object."""
s.c = master
s.ui = ui
s.window = window
s.x = x
s.y = y
@ -212,7 +214,7 @@ class CursesThreadFrame:
if self.getcolor() == 'black':
self.window.addstr(self.y, self.x, ' ', self.color)
else:
self.window.addstr(self.y, self.x, '.', self.color)
self.window.addstr(self.y, self.x, self.ui.config.getdefault("ui.Curses.Blinkenlights", "statuschar", '.'), self.color)
self.c.stdscr.move(self.c.height - 1, self.c.width - 1)
self.window.refresh()
self.c.locked(lockedstuff)
@ -476,7 +478,7 @@ class Blinkenlights(BlinkenBase, UIBase):
return s.af[accountname]
# New one.
s.af[accountname] = CursesAccountFrame(s.c, accountname)
s.af[accountname] = CursesAccountFrame(s.c, accountname, s)
s.c.lock()
try:
s.c.reset()

View File

@ -1,177 +0,0 @@
# Copyright (C) 2007 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import offlineimap.version
import urllib, sys, re, time, traceback, threading, thread
from UIBase import UIBase
from threading import *
protocol = '6.0.0'
class MachineUI(UIBase):
def __init__(s, config, verbose = 0):
UIBase.__init__(s, config, verbose)
s.safechars=" ;,./-_=+()[]"
s.iswaiting = 0
s.outputlock = Lock()
s._printData('__init__', protocol)
def isusable(s):
return True
def _printData(s, command, data, dolock = True):
s._printDataOut('msg', command, data, dolock)
def _printWarn(s, command, data, dolock = True):
s._printDataOut('warn', command, data, dolock)
def _printDataOut(s, datatype, command, data, dolock = True):
if dolock:
s.outputlock.acquire()
try:
print "%s:%s:%s:%s" % \
(datatype,
urllib.quote(command, s.safechars),
urllib.quote(currentThread().getName(), s.safechars),
urllib.quote(data, s.safechars))
sys.stdout.flush()
finally:
if dolock:
s.outputlock.release()
def _display(s, msg):
s._printData('_display', msg)
def warn(s, msg, minor):
s._printData('warn', '%s\n%d' % (msg, int(minor)))
def registerthread(s, account):
UIBase.registerthread(s, account)
s._printData('registerthread', account)
def unregisterthread(s, thread):
UIBase.unregisterthread(s, thread)
s._printData('unregisterthread', thread.getName())
def debugging(s, debugtype):
s._printData('debugging', debugtype)
def acct(s, accountname):
s._printData('acct', accountname)
def acctdone(s, accountname):
s._printData('acctdone', accountname)
def validityproblem(s, folder):
s._printData('validityproblem', "%s\n%s\n%s\n%s" % \
(folder.getname(), folder.getrepository().getname(),
folder.getsaveduidvalidity(), folder.getuidvalidity()))
def connecting(s, hostname, port):
s._printData('connecting', "%s\n%s" % (hostname, str(port)))
def syncfolders(s, srcrepos, destrepos):
s._printData('syncfolders', "%s\n%s" % (s.getnicename(srcrepos),
s.getnicename(destrepos)))
def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
s._printData('syncingfolder', "%s\n%s\n%s\n%s\n" % \
(s.getnicename(srcrepos), srcfolder.getname(),
s.getnicename(destrepos), destfolder.getname()))
def loadmessagelist(s, repos, folder):
s._printData('loadmessagelist', "%s\n%s" % (s.getnicename(repos),
folder.getvisiblename()))
def messagelistloaded(s, repos, folder, count):
s._printData('messagelistloaded', "%s\n%s\n%d" % \
(s.getnicename(repos), folder.getname(), count))
def syncingmessages(s, sr, sf, dr, df):
s._printData('syncingmessages', "%s\n%s\n%s\n%s\n" % \
(s.getnicename(sr), sf.getname(), s.getnicename(dr),
df.getname()))
def copyingmessage(s, uid, src, destlist):
ds = s.folderlist(destlist)
s._printData('copyingmessage', "%d\n%s\n%s\n%s" % \
(uid, s.getnicename(src), src.getname(), ds))
def folderlist(s, list):
return ("\f".join(["%s\t%s" % (s.getnicename(x), x.getname()) for x in list]))
def deletingmessage(s, uid, destlist):
s.deletingmessages(s, [uid], destlist)
def uidlist(s, list):
return ("\f".join([str(u) for u in list]))
def deletingmessages(s, uidlist, destlist):
ds = s.folderlist(destlist)
s._printData('deletingmessages', "%s\n%s" % (s.uidlist(uidlist), ds))
def addingflags(s, uidlist, flags, destlist):
ds = s.folderlist(destlist)
s._printData("addingflags", "%s\n%s\n%s" % (s.uidlist(uidlist),
"\f".join(flags),
ds))
def deletingflags(s, uidlist, flags, destlist):
ds = s.folderlist(destlist)
s._printData('deletingflags', "%s\n%s\n%s" % (s.uidlist(uidlist),
"\f".join(flags),
ds))
def threadException(s, thread):
print s.getThreadExceptionString(thread)
s._printData('threadException', "%s\n%s" % \
(thread.getName(), s.getThreadExceptionString(thread)))
s.delThreadDebugLog(thread)
s.terminate(100)
def terminate(s, exitstatus = 0, errortitle = '', errormsg = ''):
s._printData('terminate', "%d\n%s\n%s" % (exitstatus, errortitle, errormsg))
sys.exit(exitstatus)
def mainException(s):
s._printData('mainException', s.getMainExceptionString())
def threadExited(s, thread):
s._printData('threadExited', thread.getName())
UIBase.threadExited(s, thread)
def sleeping(s, sleepsecs, remainingsecs):
s._printData('sleeping', "%d\n%d" % (sleepsecs, remainingsecs))
if sleepsecs > 0:
time.sleep(sleepsecs)
return 0
def getpass(s, accountname, config, errmsg = None):
s.outputlock.acquire()
try:
if errmsg:
s._printData('getpasserror', "%s\n%s" % (accountname, errmsg),
False)
s._printData('getpass', accountname, False)
return (sys.stdin.readline()[:-1])
finally:
s.outputlock.release()
def init_banner(s):
s._printData('initbanner', offlineimap.version.banner)

543
offlineimap/ui/Tk.py Normal file
View File

@ -0,0 +1,543 @@
# Tk UI
# Copyright (C) 2002, 2003 John Goerzen
# <jgoerzen@complete.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from __future__ import nested_scopes
from Tkinter import *
import tkFont
from threading import *
import thread, traceback, time, threading
from StringIO import StringIO
from ScrolledText import ScrolledText
from offlineimap import threadutil, version
from Queue import Queue
from UIBase import UIBase
from offlineimap.ui.Blinkenlights import BlinkenBase
usabletest = None
class PasswordDialog:
def __init__(self, accountname, config, master=None, errmsg = None):
self.top = Toplevel(master)
self.top.title(version.productname + " Password Entry")
text = ''
if errmsg:
text = '%s: %s\n' % (accountname, errmsg)
text += "%s: Enter password: " % accountname
self.label = Label(self.top, text = text)
self.label.pack()
self.entry = Entry(self.top, show='*')
self.entry.bind("<Return>", self.ok)
self.entry.pack()
self.entry.focus_force()
self.button = Button(self.top, text = "OK", command=self.ok)
self.button.pack()
self.entry.focus_force()
self.top.wait_window(self.label)
def ok(self, args = None):
self.password = self.entry.get()
self.top.destroy()
def getpassword(self):
return self.password
class TextOKDialog:
def __init__(self, title, message, blocking = 1, master = None):
if not master:
self.top = Tk()
else:
self.top = Toplevel(master)
self.top.title(title)
self.text = ScrolledText(self.top, font = "Courier 10")
self.text.pack()
self.text.insert(END, message)
self.text['state'] = DISABLED
self.button = Button(self.top, text = "OK", command=self.ok)
self.button.pack()
if blocking:
self.top.wait_window(self.button)
def ok(self):
self.top.destroy()
class ThreadFrame(Frame):
def __init__(self, master=None):
self.threadextraframe = None
self.thread = currentThread()
self.threadid = thread.get_ident()
Frame.__init__(self, master, relief = RIDGE, borderwidth = 2)
self.pack(fill = 'x')
self.threadlabel = Label(self, foreground = '#FF0000',
text ="Thread %d (%s)" % (self.threadid,
self.thread.getName()))
self.threadlabel.pack()
self.setthread(currentThread())
self.account = "Unknown"
self.mailbox = "Unknown"
self.loclabel = Label(self,
text = "Account/mailbox information unknown")
#self.loclabel.pack()
self.updateloclabel()
self.message = Label(self, text="Messages will appear here.\n",
foreground = '#0000FF')
self.message.pack(fill = 'x')
def setthread(self, newthread):
if newthread:
self.threadlabel['text'] = newthread.getName()
else:
self.threadlabel['text'] = "No thread"
self.destroythreadextraframe()
def destroythreadextraframe(self):
if self.threadextraframe:
self.threadextraframe.destroy()
self.threadextraframe = None
def getthreadextraframe(self):
if self.threadextraframe:
return self.threadextraframe
self.threadextraframe = Frame(self)
self.threadextraframe.pack(fill = 'x')
return self.threadextraframe
def setaccount(self, account):
self.account = account
self.mailbox = "Unknown"
self.updateloclabel()
def setmailbox(self, mailbox):
self.mailbox = mailbox
self.updateloclabel()
def updateloclabel(self):
self.loclabel['text'] = "Processing %s: %s" % (self.account,
self.mailbox)
def appendmessage(self, newtext):
self.message['text'] += "\n" + newtext
def setmessage(self, newtext):
self.message['text'] = newtext
class VerboseUI(UIBase):
def isusable(s):
global usabletest
if usabletest != None:
return usabletest
try:
Tk().destroy()
usabletest = 1
except TclError:
usabletest = 0
return usabletest
def _createTopWindow(self, doidlevac = 1):
self.notdeleted = 1
self.created = threading.Event()
self.af = {}
self.aflock = Lock()
t = threadutil.ExitNotifyThread(target = self._runmainloop,
name = "Tk Mainloop")
t.setDaemon(1)
t.start()
self.created.wait()
del self.created
if doidlevac:
t = threadutil.ExitNotifyThread(target = self.idlevacuum,
name = "Tk idle vacuum")
t.setDaemon(1)
t.start()
def _runmainloop(s):
s.top = Tk()
s.top.title(version.productname + " " + version.versionstr)
s.top.after_idle(s.created.set)
s.top.mainloop()
s.notdeleted = 0
def getaccountframe(s):
accountname = s.getthreadaccount()
s.aflock.acquire()
try:
if accountname in s.af:
return s.af[accountname]
s.af[accountname] = LEDAccountFrame(s.top, accountname,
s.fontfamily, s.fontsize)
finally:
s.aflock.release()
return s.af[accountname]
def getpass(s, accountname, config, errmsg = None):
pd = PasswordDialog(accountname, config, errmsg = errmsg)
return pd.getpassword()
def gettf(s, newtype=ThreadFrame, master = None):
if master == None:
master = s.top
threadid = thread.get_ident()
s.tflock.acquire()
try:
if threadid in s.threadframes:
return s.threadframes[threadid]
if len(s.availablethreadframes):
tf = s.availablethreadframes.pop(0)
tf.setthread(currentThread())
else:
tf = newtype(master)
s.threadframes[threadid] = tf
return tf
finally:
s.tflock.release()
def _display(s, msg):
s.gettf().setmessage(msg)
def threadExited(s, thread):
threadid = thread.threadid
s.tflock.acquire()
if threadid in s.threadframes:
tf = s.threadframes[threadid]
tf.setthread(None)
tf.setaccount("Unknown")
tf.setmessage("Idle")
s.availablethreadframes.append(tf)
del s.threadframes[threadid]
s.tflock.release()
UIBase.threadExited(s, thread)
def idlevacuum(s):
while s.notdeleted:
time.sleep(10)
s.tflock.acquire()
while len(s.availablethreadframes):
tf = s.availablethreadframes.pop()
tf.destroy()
s.tflock.release()
def terminate(s, exitstatus = 0, errortitle = None, errormsg = None):
if errormsg <> None:
if errortitle == None:
errortitle = "Error"
TextOKDialog(errortitle, errormsg)
UIBase.terminate(s, exitstatus = exitstatus, errortitle = None, errormsg = None)
def threadException(s, thread):
exceptionstr = s.getThreadExceptionString(thread)
print exceptionstr
s.top.destroy()
s.top = None
TextOKDialog("Thread Exception", exceptionstr)
s.delThreadDebugLog(thread)
s.terminate(100)
def mainException(s):
exceptionstr = s.getMainExceptionString()
print exceptionstr
s.top.destroy()
s.top = None
TextOKDialog("Main Program Exception", exceptionstr)
def warn(s, msg, minor = 0):
if minor:
# Just let the default handler catch it
UIBase.warn(s, msg, minor)
else:
TextOKDialog("OfflineIMAP Warning", msg)
def showlicense(s):
TextOKDialog(version.productname + " License",
version.bigcopyright + "\n" +
version.homepage + "\n\n" + version.license,
blocking = 0, master = s.top)
def init_banner(s):
s.threadframes = {}
s.availablethreadframes = []
s.tflock = Lock()
s._createTopWindow()
s._msg(version.productname + " " + version.versionstr + ", " +\
version.copyright)
tf = s.gettf().getthreadextraframe()
b = Button(tf, text = "About", command = s.showlicense)
b.pack(side = LEFT)
b = Button(tf, text = "Exit", command = s.terminate)
b.pack(side = RIGHT)
s.sleeping_abort = {}
def deletingmessages(s, uidlist, destlist):
ds = s.folderlist(destlist)
s._msg("Deleting %d messages in %s" % (len(uidlist), ds))
def _sleep_cancel(s, args = None):
s.sleeping_abort[thread.get_ident()] = 1
def sleep(s, sleepsecs):
threadid = thread.get_ident()
s.sleeping_abort[threadid] = 0
tf = s.gettf().getthreadextraframe()
def sleep_cancel():
s.sleeping_abort[threadid] = 1
sleepbut = Button(tf, text = 'Sync immediately',
command = sleep_cancel)
sleepbut.pack()
UIBase.sleep(s, sleepsecs)
def sleeping(s, sleepsecs, remainingsecs):
retval = s.sleeping_abort[thread.get_ident()]
if remainingsecs:
s._msg("Next sync in %d:%02d" % (remainingsecs / 60,
remainingsecs % 60))
else:
s._msg("Wait done; synchronizing now.")
s.gettf().destroythreadextraframe()
del s.sleeping_abort[thread.get_ident()]
time.sleep(sleepsecs)
return retval
TkUI = VerboseUI
################################################## Blinkenlights
class LEDAccountFrame:
def __init__(self, top, accountname, fontfamily, fontsize):
self.top = top
self.accountname = accountname
self.fontfamily = fontfamily
self.fontsize = fontsize
self.frame = Frame(self.top, background = 'black')
self.frame.pack(side = BOTTOM, expand = 1, fill = X)
self._createcanvas(self.frame)
self.label = Label(self.frame, text = accountname,
background = "black", foreground = "blue",
font = (self.fontfamily, self.fontsize))
self.label.grid(sticky = E, row = 0, column = 1)
def getnewthreadframe(s):
return LEDThreadFrame(s.canvas)
def _createcanvas(self, parent):
c = LEDFrame(parent)
self.canvas = c
c.grid(sticky = E, row = 0, column = 0)
parent.grid_columnconfigure(1, weight = 1)
#c.pack(side = LEFT, expand = 0, fill = X)
def startsleep(s, sleepsecs):
s.sleeping_abort = 0
s.button = Button(s.frame, text = "Sync now", command = s.syncnow,
background = "black", activebackground = "black",
activeforeground = "white",
foreground = "blue", highlightthickness = 0,
padx = 0, pady = 0,
font = (s.fontfamily, s.fontsize), borderwidth = 0,
relief = 'solid')
s.button.grid(sticky = E, row = 0, column = 2)
def syncnow(s):
s.sleeping_abort = 1
def sleeping(s, sleepsecs, remainingsecs):
if remainingsecs:
s.button.config(text = 'Sync now (%d:%02d remain)' % \
(remainingsecs / 60, remainingsecs % 60))
time.sleep(sleepsecs)
else:
s.button.destroy()
del s.button
return s.sleeping_abort
class LEDFrame(Frame):
"""This holds the different lights."""
def getnewobj(self):
retval = Canvas(self, background = 'black', height = 20, bd = 0,
highlightthickness = 0, width = 10)
retval.pack(side = LEFT, padx = 0, pady = 0, ipadx = 0, ipady = 0)
return retval
class LEDThreadFrame:
"""There is one of these for each little light."""
def __init__(self, master):
self.canvas = master.getnewobj()
self.color = ''
self.ovalid = self.canvas.create_oval(4, 4, 9,
9, fill = 'gray',
outline = '#303030')
def setcolor(self, newcolor):
if newcolor != self.color:
self.canvas.itemconfigure(self.ovalid, fill = newcolor)
self.color = newcolor
def getcolor(self):
return self.color
def setthread(self, newthread):
if newthread:
self.setcolor('gray')
else:
self.setcolor('black')
class Blinkenlights(BlinkenBase, VerboseUI):
def __init__(s, config, verbose = 0):
VerboseUI.__init__(s, config, verbose)
s.fontfamily = 'Helvetica'
s.fontsize = 8
if config.has_option('ui.Tk.Blinkenlights', 'fontfamily'):
s.fontfamily = config.get('ui.Tk.Blinkenlights', 'fontfamily')
if config.has_option('ui.Tk.Blinkenlights', 'fontsize'):
s.fontsize = config.getint('ui.Tk.Blinkenlights', 'fontsize')
def isusable(s):
return VerboseUI.isusable(s)
def _createTopWindow(self):
VerboseUI._createTopWindow(self, 0)
#self.top.resizable(width = 0, height = 0)
self.top.configure(background = 'black', bd = 0)
widthmetric = tkFont.Font(family = self.fontfamily, size = self.fontsize).measure("0")
self.loglines = self.config.getdefaultint("ui.Tk.Blinkenlights",
"loglines", 5)
self.bufferlines = self.config.getdefaultint("ui.Tk.Blinkenlights",
"bufferlines", 500)
self.text = ScrolledText(self.top, bg = 'black', #scrollbar = 'y',
font = (self.fontfamily, self.fontsize),
bd = 0, highlightthickness = 0, setgrid = 0,
state = DISABLED, height = self.loglines,
wrap = NONE, width = 60)
self.text.vbar.configure(background = '#000050',
activebackground = 'blue',
highlightbackground = 'black',
troughcolor = "black", bd = 0,
elementborderwidth = 2)
self.textenabled = 0
self.tags = []
self.textlock = Lock()
def init_banner(s):
BlinkenBase.init_banner(s)
s._createTopWindow()
menubar = Menu(s.top, activebackground = "black",
activeforeground = "white",
activeborderwidth = 0,
background = "black", foreground = "blue",
font = (s.fontfamily, s.fontsize), bd = 0)
menubar.add_command(label = "About", command = s.showlicense)
menubar.add_command(label = "Show Log", command = s._togglelog)
menubar.add_command(label = "Exit", command = s.terminate)
s.top.config(menu = menubar)
s.menubar = menubar
s.text.see(END)
if s.config.getdefaultboolean("ui.Tk.Blinkenlights", "showlog", 1):
s._togglelog()
s.gettf().setcolor('red')
s.top.resizable(width = 0, height = 0)
s._msg(version.banner)
def _togglelog(s):
if s.textenabled:
s.oldtextheight = s.text.winfo_height()
s.text.pack_forget()
s.textenabled = 0
s.menubar.entryconfig('Hide Log', label = 'Show Log')
s.top.update()
s.top.geometry("")
s.top.update()
s.top.resizable(width = 0, height = 0)
s.top.update()
else:
s.text.pack(side = TOP, expand = 1, fill = BOTH)
s.textenabled = 1
s.top.update()
s.top.geometry("")
s.menubar.entryconfig('Show Log', label = 'Hide Log')
s._rescroll()
s.top.resizable(width = 1, height = 1)
def sleep(s, sleepsecs):
s.gettf().setcolor('red')
s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
BlinkenBase.sleep(s, sleepsecs)
def sleeping(s, sleepsecs, remainingsecs):
return BlinkenBase.sleeping(s, sleepsecs, remainingsecs)
def _rescroll(s):
s.text.see(END)
lo, hi = s.text.vbar.get()
s.text.vbar.set(1.0 - (hi - lo), 1.0)
def _display(s, msg):
if "\n" in msg:
for thisline in msg.split("\n"):
s._msg(thisline)
return
#VerboseUI._msg(s, msg)
color = s.gettf().getcolor()
rescroll = 1
s.textlock.acquire()
try:
if s.text.vbar.get()[1] != 1.0:
rescroll = 0
s.text.config(state = NORMAL)
if not color in s.tags:
s.text.tag_config(color, foreground = color)
s.tags.append(color)
s.text.insert(END, "\n" + msg, color)
# Trim down. Not quite sure why I have to say 7 instead of 5,
# but so it is.
while float(s.text.index(END)) > s.bufferlines + 2.0:
s.text.delete(1.0, 2.0)
if rescroll:
s._rescroll()
finally:
s.text.config(state = DISABLED)
s.textlock.release()

View File

@ -151,17 +151,17 @@ class UIBase:
################################################## WARNINGS
def msgtoreadonly(s, destfolder, uid, content, flags):
if not (s.config.has_option('general', 'ignore-readonly') and s.config.getboolean("general", "ignore-readonly")):
if not (config.has_option('general', 'ignore-readonly') and config.getboolean("general", "ignore-readonly")):
s.warn("Attempted to synchronize message %d to folder %s[%s], but that folder is read-only. The message will not be copied to that folder." % \
(uid, s.getnicename(destfolder), destfolder.getname()))
def flagstoreadonly(s, destfolder, uidlist, flags):
if not (s.config.has_option('general', 'ignore-readonly') and s.config.getboolean("general", "ignore-readonly")):
if not (config.has_option('general', 'ignore-readonly') and config.getboolean("general", "ignore-readonly")):
s.warn("Attempted to modify flags for messages %s in folder %s[%s], but that folder is read-only. No flags have been modified for that message." % \
(str(uidlist), s.getnicename(destfolder), destfolder.getname()))
def deletereadonly(s, destfolder, uidlist):
if not (s.config.has_option('general', 'ignore-readonly') and s.config.getboolean("general", "ignore-readonly")):
if not (config.has_option('general', 'ignore-readonly') and config.getboolean("general", "ignore-readonly")):
s.warn("Attempted to delete messages %s in folder %s[%s], but that folder is read-only. No messages have been deleted in that folder." % \
(str(uidlist), s.getnicename(destfolder), destfolder.getname()))
@ -208,10 +208,9 @@ class UIBase:
s.getnicename(srcrepos),
s.getnicename(destrepos)))
def validityproblem(s, folder):
s.warn("UID validity problem for folder %s (repo %s) (saved %d; got %d); skipping it" % \
(folder.getname(), folder.getrepository().getname(),
folder.getsaveduidvalidity(), folder.getuidvalidity()))
def validityproblem(s, folder, saved, new):
s.warn("UID validity problem for folder %s (saved %d; got %d); skipping it" % \
(folder.getname(), saved, new))
def loadmessagelist(s, repos, folder):
if s.verbose > 0:

View File

@ -23,6 +23,13 @@ try:
except ImportError:
pass
try:
import Tkinter
except ImportError:
pass
else:
import Tk
try:
import curses
except ImportError:
@ -31,7 +38,6 @@ else:
import Curses
import Noninteractive
import Machine
# Must be last
import detector

View File

@ -19,9 +19,9 @@
import offlineimap.ui
import sys
DEFAULT_UI_LIST = ('Curses.Blinkenlights', 'TTY.TTYUI',
'Noninteractive.Basic', 'Noninteractive.Quiet',
'Machine.MachineUI')
DEFAULT_UI_LIST = ('Tk.Blinkenlights', 'Tk.VerboseUI',
'Curses.Blinkenlights', 'TTY.TTYUI',
'Noninteractive.Basic', 'Noninteractive.Quiet')
def findUI(config, chosenUI=None):
uistrlist = list(DEFAULT_UI_LIST)

View File

@ -1,11 +1,11 @@
productname = 'OfflineIMAP'
versionstr = "5.99.0"
versionstr = "4.0.16"
versionlist = versionstr.split(".")
major = versionlist[0]
minor = versionlist[1]
patch = versionlist[2]
copyright = "Copyright (C) 2002 - 2007 John Goerzen"
copyright = "Copyright (C) 2002 - 2006 John Goerzen"
author = "John Goerzen"
author_email = "jgoerzen@complete.org"
description = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
@ -18,7 +18,7 @@ COPYING for details. This is free software, and you are welcome
to distribute it under the conditions laid out in COPYING."""
homepage = "http://software.complete.org/offlineimap/"
license = """Copyright (C) 2002 - 2007 John Goerzen <jgoerzen@complete.org>
license = """Copyright (C) 2002 - 2006 John Goerzen <jgoerzen@complete.org>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -34,13 +34,7 @@ You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA"""
def getcmdhelp():
from offlineimap.ui import detector
import os
uilist = ""
for ui in detector.DEFAULT_UI_LIST:
uilist += " " + ui + os.linesep
return """
cmdhelp = """
offlineimap [ -1 ] [ -P profiledir ] [ -a accountlist ] [
-c configfile ] [ -d debugtype[,debugtype...] ] [ -o ] [
-u interface ]
@ -108,4 +102,8 @@ def getcmdhelp():
states that it cannot be. Use this option with
care. The pre-defined options, described in the
USER INTERFACES section of the man page, are:
""" + uilist
"""
from offlineimap.ui import detector
import os
for ui in detector.DEFAULT_UI_LIST:
cmdhelp += " " + ui + os.linesep