...
 
Commits (17)
......@@ -27,13 +27,16 @@ Birdtray is a free system tray notification for new mail for Thunderbird. Its pr
- Has configurable "New Email" functionality, allowing pre-configured email templates.
## Compiling
## Building
Currently the only way to test Birdtray is to build it from source, which should be easy on Linux, but may be cumbersome on other OSes. You would need the following libraries:
To build Birdtray from source, you would need the following libraries:
- Qt 5.5 or higher with "x11extras-dev" or "x11extras-devel" module installed (it is usually NOT installed by default);
- sqlite3 (i.e. libsqlite3-dev or libsqlite3-devel)
- On Debian you need to install the following packages: qt5-defaults libsqlite3-dev libqt5x11extras-dev
On Debian you need to install the following packages: ``qt5-defaults libsqlite3-dev libqt5x11extras-dev``
On OpenSuSE you need to install ``libqt5-qtbase-devel libqt5-qtx11extras-devel sqlite3-devel``
To build, please do the following:
......@@ -44,18 +47,17 @@ To build, please do the following:
Launch the ./birdtray executable from the local directory. It will show Thunderbird icon in system tray.
Right-click on this icon, and click Settings. Select the Thunderbird profile folder - must contain the file "global-messages-db.sqlite".
Then select the font and default color (which will be used if more than one monitored folder has new mail).
Right-click on this icon, and click Settings. Go to Monitoring tab ans select the Thunderbird MSF file for the mailbox you'd like to monitor. You can specify different notification colors for each mailbox. Birdtray will show the new email count using this color if only this folder has new mail. If more than one folder has new mail, the default color will be used.
Then click on Accounts tab, and add one or more folders. You can specify different notification colors for each folder. Birdtray will show the new email count using this color if only this folder has new mail. If more than one folder has new mail, the default color will be used.
Then select the font and default color (which will be used if more than one monitored folder has new mail).
You can also enable birdtray to start Thunderbird when you start Birdtray, or enable show/hide Thunderbird when the system tray icon is clicked, in settings.
Once you change settings, you must restart birdtray for the new settings to take effect.
Once you change settings, often you need to restart birdtray for the new settings to take effect.
Because Thunderbird updates database after 5-10 seconds when the emails are read or new emails arrived, there is a delay between a new email is arrived and birdtray "sees" it. This is a known issue which currently I do not know how to address.
## Troubleshooting
If you have no unread email, but Birdtray still shows there is unread email, this is caused by Thunderbird not properly updating the messages database. Use the "Fix" button in Settings dialog to fix that.
If you have lots of unread messages shown, and you are using global search database to look for unread messages, it may be because the database is corrupt or too old. You may delete the file global-messages-db.sqlite and restart Thunderbird which would rebuild this file. This will also help if "search" function in Thunderbird finds emails which no longer exist.
## Submitting bugs and feature requests
......
birdtray (1.5-1) unstable; urgency=medium
* New upstream release.
-- Adam Borowski <kilobyte@angband.pl> Sat, 16 Feb 2019 23:16:33 +0100
birdtray (1.4-3) unstable; urgency=medium
* Add epoch to the versioned build-dependency.
......
......@@ -27,5 +27,5 @@ Description: system tray notifications for Thunderbird
insides, it suffers from problems like noticing new mails only after a
delay, having to restart Thunderbird just to hide its window, etc --
you'd want to use an extension like firetray instead -- but, it is
likely that support for Thunderbird extensions will be dropped soon,
likely that support for Thunderbird XUL extensions will be dropped soon,
possibly by the time you read these words.
......@@ -42,7 +42,8 @@ SOURCES += \
setting_newemail.cpp \
modelnewemails.cpp \
modelaccounttree.cpp \
morkparser.cpp
morkparser.cpp \
utils.cpp
HEADERS += \
trayicon.h \
......@@ -61,7 +62,8 @@ HEADERS += \
setting_newemail.h \
modelnewemails.h \
modelaccounttree.h \
morkparser.h
morkparser.h \
utils.h
FORMS += \
dialogaddeditaccount.ui \
......
......@@ -36,6 +36,7 @@ DialogSettings::DialogSettings( QWidget *parent)
connect( btnAccountAdd, &QPushButton::clicked, this, &DialogSettings::accountAdd );
connect( btnAccountEdit, &QPushButton::clicked, this, &DialogSettings::accountEdit );
connect( btnAccountRemove, &QPushButton::clicked, this, &DialogSettings::accountRemove );
connect( btnAccountAddMultiple, &QPushButton::clicked, this, &DialogSettings::accountAddMultiple );
connect( treeNewEmails, &QTreeView::doubleClicked, this, &DialogSettings::newEmailEditIndex );
connect( btnNewEmailAdd, &QPushButton::clicked, this, &DialogSettings::newEmailAdd );
......@@ -62,6 +63,9 @@ DialogSettings::DialogSettings( QWidget *parent)
boxEnableNewEmail->setChecked( pSettings->mNewEmailMenuEnabled );
boxBlinkingUsesAlpha->setChecked( pSettings->mBlinkingUseAlphaTransition );
boxAllowSuppression->setChecked( pSettings->mAllowSuppressingUnreads );
spinUnreadOpacityLevel->setValue( pSettings->mUnreadOpacityLevel * 100 );
spinThunderbirdStartDelay->setValue( pSettings->mLaunchThunderbirdDelay );
boxShowUnreadCount->setChecked( pSettings->mShowUnreadEmailCount );
if ( pSettings->mLaunchThunderbird )
boxStopThunderbirdOnExit->setChecked( pSettings->mExitThunderbirdWhenQuit );
......@@ -161,6 +165,9 @@ void DialogSettings::accept()
pSettings->mNewEmailMenuEnabled = boxEnableNewEmail->isChecked();
pSettings->mBlinkingUseAlphaTransition = boxBlinkingUsesAlpha->isChecked();
pSettings->mUseMorkParser = isMorkParserSelected();
pSettings->mUnreadOpacityLevel = (double) spinUnreadOpacityLevel->value() / 100.0;
pSettings->mLaunchThunderbirdDelay = spinThunderbirdStartDelay->value();
pSettings->mShowUnreadEmailCount = boxShowUnreadCount->isChecked();
pSettings->mNotificationIcon = btnNotificationIcon->icon().pixmap( pSettings->mIconSize );
......@@ -280,9 +287,25 @@ void DialogSettings::accountAdd()
mAccountModel->addAccount( dlg.account(), dlg.color() );
}
void DialogSettings::accountAddMultiple()
{
QStringList files = QFileDialog::getOpenFileNames( 0,
"Choose one or more MSF files",
"",
"Mail Index (*.msf)" );
if ( files.isEmpty() )
return;
// Add them all with default color
for ( QString f : files )
mAccountModel->addAccount( f, btnNotificationColor->color() );
}
void DialogSettings::accountEdit()
{
accountEditIndex( treeAccounts->currentIndex() );
if ( treeAccounts->currentIndex().isValid() )
accountEditIndex( treeAccounts->currentIndex() );
}
void DialogSettings::accountEditIndex(const QModelIndex &index)
......@@ -367,6 +390,9 @@ void DialogSettings::unreadParserChanged(int curr)
// Hide the thunderbird path as well
groupThunderbirdProfilePath->hide();
// Show the "add multiple" button
btnAccountAddMultiple->setVisible( true );
}
else
{
......@@ -377,6 +403,9 @@ void DialogSettings::unreadParserChanged(int curr)
// Trigger hiding/showing the account group
profilePathChanged();
// Hide the "add multiple" button
btnAccountAddMultiple->setHidden( true );
}
// Did we change comparing to settings?
......
......@@ -38,6 +38,7 @@ class DialogSettings : public QDialog, public Ui::DialogSettings
// Account buttons
void accountAdd();
void accountAddMultiple();
void accountEdit();
void accountEditIndex( const QModelIndex& index );
void accountRemove();
......
This diff is collapsed.
......@@ -4,6 +4,7 @@
#include "trayicon.h"
#include "settings.h"
#include "morkparser.h"
#include "utils.h"
int main(int argc, char *argv[])
{
......@@ -15,6 +16,12 @@ int main(int argc, char *argv[])
return 1;
}
if ( argc == 3 && !strcmp( argv[1], "--decode" ) )
{
printf( "Decoded: %s\n", qPrintable( Utils::decodeIMAPutf7( argv[2] )));
return 1;
}
if ( !QSystemTrayIcon::isSystemTrayAvailable() )
qFatal( "Sorry, system tray cannot be controlled through this addon on your operating system");
......
......@@ -2,13 +2,17 @@
#include "settings.h"
#include "modelaccounttree.h"
#include "utils.h"
ModelAccountTree::ModelAccountTree( QObject *parent )
: QAbstractItemModel( parent )
{
// Get the current settings
mAccounts = pSettings->mFolderNotificationColors.keys();
mColors = pSettings->mFolderNotificationColors.values();
// Get the current settings in proper(stored) order
for ( QString uri : pSettings->mFolderNotificationList )
{
mAccounts.push_back( uri );
mColors.push_back( pSettings->mFolderNotificationColors[uri] );
}
}
int ModelAccountTree::columnCount(const QModelIndex &) const
......@@ -24,13 +28,14 @@ QVariant ModelAccountTree::data(const QModelIndex &index, int role) const
if ( role == Qt::DisplayRole )
{
if ( index.column() == 0 )
return mAccounts[index.row()];
return Utils::decodeIMAPutf7( mAccounts[index.row()] );
else
return "uses this color";
}
if ( role == Qt::BackgroundRole && index.column() == 1 )
else if ( role == Qt::BackgroundRole && index.column() == 1 )
return QBrush( mColors[index.row()] );
else if ( role == Qt::ToolTipRole && index.column() == 0 )
return mAccounts[index.row()];
}
return QVariant();
......@@ -118,9 +123,11 @@ void ModelAccountTree::clear()
void ModelAccountTree::applySettings()
{
pSettings->mFolderNotificationColors.clear();
pSettings->mFolderNotificationList.clear();
for ( int i = 0; i < mAccounts.size(); i++ )
{
pSettings->mFolderNotificationList.push_back( mAccounts[i] );
pSettings->mFolderNotificationColors[ mAccounts[i] ] = mColors[i];
}
}
......@@ -74,12 +74,15 @@ void ModelNewEmails::add()
void ModelNewEmails::edit(const QModelIndex &idx)
{
if ( mNewEmailData[ idx.row() ].edit() )
if ( idx.isValid() && mNewEmailData[ idx.row() ].edit() )
emit dataChanged( createIndex( idx.row(), 0 ), createIndex( idx.row(), 1 ) );
}
void ModelNewEmails::remove(const QModelIndex &idx)
{
if ( !idx.isValid() )
return;
beginRemoveRows( QModelIndex(), idx.row(), idx.row() );
mNewEmailData.removeAt( idx.row() );
endRemoveRows();
......
......@@ -29,6 +29,8 @@ void Settings::save()
settings.setValue("common/hidewhenstarted", mHideWhenStarted );
settings.setValue("common/hidewhenrestarted", mHideWhenRestarted );
settings.setValue("common/allowsuppressingunread", mAllowSuppressingUnreads );
settings.setValue("common/launchthunderbirddelay", mLaunchThunderbirdDelay );
settings.setValue("common/showunreademailcount", mShowUnreadEmailCount );
settings.setValue("advanced/tbcmdline", mThunderbirdCmdLine );
settings.setValue("advanced/tbwindowmatch", mThunderbirdWindowMatch );
......@@ -37,12 +39,13 @@ void Settings::save()
settings.setValue("advanced/notificationfontmaxsize", mNotificationMaximumFontSize );
settings.setValue("advanced/watchfiletimeout", mWatchFileTimeout );
settings.setValue("advanced/blinkingusealpha", mBlinkingUseAlphaTransition );
settings.setValue("advanced/unreadopacitylevel", mUnreadOpacityLevel );
// Convert the map into settings
settings.setValue("accounts/count", mFolderNotificationColors.size() );
int index = 0;
for ( QString uri : mFolderNotificationColors.keys() )
for ( QString uri : mFolderNotificationList )
{
QString entry = "accounts/account" + QString::number( index );
settings.setValue( entry + "Color", mFolderNotificationColors[uri].name() );
......@@ -98,6 +101,8 @@ void Settings::load()
mHideWhenStarted = settings.value("common/hidewhenstarted", false ).toBool();
mHideWhenRestarted = settings.value("common/hidewhenrestarted", false ).toBool();
mAllowSuppressingUnreads = settings.value("common/allowsuppressingunread", false ).toBool();
mLaunchThunderbirdDelay = settings.value("common/launchthunderbirddelay", 0 ).toInt();
mShowUnreadEmailCount = settings.value("common/showunreademailcount", true ).toBool();
mThunderbirdCmdLine = settings.value("advanced/tbcmdline", "/usr/bin/thunderbird" ).toString();
mThunderbirdWindowMatch = settings.value("advanced/tbwindowmatch", "- Mozilla Thunderbird" ).toString();
......@@ -106,6 +111,7 @@ void Settings::load()
mUseMorkParser = settings.value("advanced/unreadmorkparser", true ).toBool();
mWatchFileTimeout = settings.value("advanced/watchfiletimeout", 150 ).toUInt();
mBlinkingUseAlphaTransition = settings.value("advanced/blinkingusealpha", false ).toBool();
mUnreadOpacityLevel = settings.value("advanced/unreadopacitylevel", 0.75 ).toDouble();
mFolderNotificationColors.clear();
......@@ -115,7 +121,9 @@ void Settings::load()
for ( int index = 0; index < total; index++ )
{
QString entry = "accounts/account" + QString::number( index );
mFolderNotificationColors[ settings.value( entry + "URI", "" ).toString() ] = QColor( settings.value( entry + "Color", "" ).toString() );
QString key = settings.value( entry + "URI", "" ).toString();
mFolderNotificationColors[ key ] = QColor( settings.value( entry + "Color", "" ).toString() );
mFolderNotificationList.push_back( key );
}
// Load new email data from settings
......
......@@ -37,6 +37,9 @@ class Settings
// Blinking speed
unsigned int mBlinkSpeed;
// Opacity level for the tray icon when unread email is present (0.0-1.0)
double mUnreadOpacityLevel;
// Path to Thunderbird folder
QString mThunderbirdFolderPath;
......@@ -55,6 +58,9 @@ class Settings
// Whether to launch Thunderbird when the app starts
bool mLaunchThunderbird;
// The delay in seconds to launch Thunderbird
int mLaunchThunderbirdDelay;
// Whether to hide Thunderbird window after starting
bool mHideWhenStarted;
......@@ -79,6 +85,9 @@ class Settings
// Whether to allow suppression of unread emails
bool mAllowSuppressingUnreads;
// Whether to show the unread email count
bool mShowUnreadEmailCount;
// Watching file timeout (ms)
unsigned int mWatchFileTimeout;
......@@ -90,8 +99,10 @@ class Settings
bool mNewEmailMenuEnabled;
QList< Setting_NewEmail > mNewEmailData;
// Maps the folder URI or full path (for Mork) to the notification color
// Maps the folder URI or full path (for Mork) to the notification color.
// The original order of strings is stored in mFolderNotificationList (to show in UI)
QMap< QString, QColor > mFolderNotificationColors;
QStringList mFolderNotificationList;
// Load and save them
void save();
......
......@@ -30,6 +30,8 @@ TrayIcon::TrayIcon()
mThunderbirdWindowExisted = false;
mThunderbirdWindowHide = false;
mThunderbirdStartTime = QDateTime::currentDateTime().addSecs( pSettings->mLaunchThunderbirdDelay );
mWinTools = WindowTools::create();
// If the settings are not yet configure, pop up the message
......@@ -169,7 +171,7 @@ void TrayIcon::updateIcon()
}
// Do we need to draw the unread counter?
if ( unread > 0 )
if ( unread > 0 && pSettings->mShowUnreadEmailCount )
{
// Find the suitable font size, starting from 4
QString countvalue = QString::number( unread );
......@@ -266,7 +268,7 @@ void TrayIcon::updateState()
if ( !mThunderbirdWindowExisted )
{
// No. Shall we start it?
if ( pSettings->mLaunchThunderbird && !mThunderbirdProcess )
if ( pSettings->mLaunchThunderbird && !mThunderbirdProcess && mThunderbirdStartTime < QDateTime::currentDateTime() )
{
startThunderbird();
......@@ -316,13 +318,13 @@ void TrayIcon::blinkTimeout()
if ( pSettings->mBlinkSpeed != 0 )
{
// Flip the opacity
if ( mBlinkingIconOpacity == 0.75 )
mBlinkingIconOpacity = 0.15;
if ( mBlinkingIconOpacity == pSettings->mUnreadOpacityLevel )
mBlinkingIconOpacity = 1.0 - pSettings->mUnreadOpacityLevel;
else
mBlinkingIconOpacity = 0.75;
mBlinkingIconOpacity = pSettings->mUnreadOpacityLevel;
}
else
mBlinkingIconOpacity = 0.75;
mBlinkingIconOpacity = pSettings->mUnreadOpacityLevel;
}
updateIcon();
......@@ -432,7 +434,10 @@ void TrayIcon::actionIgnoreEmails()
void TrayIcon::actionSystrayIconActivated(QSystemTrayIcon::ActivationReason reason)
{
if ( reason == QSystemTrayIcon::Trigger )
actionActivate();
{
if ( pSettings->mShowHideThunderbird )
actionActivate();
}
}
void TrayIcon::createMenu()
......@@ -553,7 +558,10 @@ void TrayIcon::startThunderbird()
mThunderbirdProcess = new QProcess();
connect( mThunderbirdProcess, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(tbProcessFinished(int,QProcess::ExitStatus)) );
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
connect( mThunderbirdProcess, &QProcess::errorOccurred, this, &TrayIcon::tbProcessError );
#endif
mThunderbirdProcess->start( pSettings->mThunderbirdCmdLine );
}
......
......@@ -96,6 +96,9 @@ class TrayIcon : public QSystemTrayIcon
// Current status
QString mCurrentStatus;
// Time when Thunderbird could be started
QDateTime mThunderbirdStartTime;
// If true, Thunderbird window existed anytime before, but not necessarily now
// (we use this to distinguish between start and restart)
bool mThunderbirdWindowExisted;
......
......@@ -254,9 +254,9 @@ int UnreadMonitor::getMorkUnreadCount(const QString &path)
MorkParser parser;
if ( !parser.open( path ) )
return -1;
return 0;
int unread = 0;
unsigned int unread = 0;
// First we parse the unreadChildren column (generic view)
const MorkRowMap * rows = parser.rows( 0x80, 0, 0x80 );
......@@ -273,9 +273,13 @@ int UnreadMonitor::getMorkUnreadCount(const QString &path)
if ( columnName == "unreadChildren" )
{
unsigned int value = parser.getValue(cells[colid ]).toInt( nullptr, 16 );
unread += value;
}
bool correct;
unsigned int value = parser.getValue(cells[colid ]).toUInt( &correct, 16 );
if ( correct )
unread += value;
else
qDebug("Incorrect Mork value: %s", qPrintable( parser.getValue(cells[colid ]) )); }
}
}
}
......@@ -296,8 +300,13 @@ int UnreadMonitor::getMorkUnreadCount(const QString &path)
if ( columnName == "numNewMsgs" )
{
unsigned int value = parser.getValue(cells[colid ]).toInt( nullptr, 16 );
unread += value;
bool correct;
unsigned int value = parser.getValue(cells[colid ]).toInt( &correct, 16 );
if ( correct )
unread += value;
else
qDebug("Incorrect Mork value: %s", qPrintable( parser.getValue(cells[colid ]) ));
}
}
}
......
#include "utils.h"
#include <QTextCodec>
QString Utils::decodeIMAPutf7(const QString &param)
{
// See https://tools.ietf.org/html/rfc2060#page-13
// By convention, international mailbox names are specified using a
// modified version of the UTF-7 encoding described in [UTF-7]. The
// purpose of these modifications is to correct the following problems
// with UTF-7:
//
// 1) UTF-7 uses the "+" character for shifting; this conflicts with
// the common use of "+" in mailbox names, in particular USENET
// newsgroup names.
//
// 2) UTF-7's encoding is BASE64 which uses the "/" character; this
// conflicts with the use of "/" as a popular hierarchy delimiter.
//
// 3) UTF-7 prohibits the unencoded usage of "\"; this conflicts with
// the use of "\" as a popular hierarchy delimiter.
//
// 4) UTF-7 prohibits the unencoded usage of "~"; this conflicts with
// the use of "~" in some servers as a home directory indicator.
//
// 5) UTF-7 permits multiple alternate forms to represent the same
// string; in particular, printable US-ASCII chararacters can be
// represented in encoded form.
//
// In modified UTF-7, printable US-ASCII characters except for "&"
// represent themselves; that is, characters with octet values 0x20-0x25
// and 0x27-0x7e. The character "&" (0x26) is represented by the two-
// octet sequence "&-".
//
// All other characters (octet values 0x00-0x1f, 0x7f-0xff, and all
// Unicode 16-bit octets) are represented in modified BASE64, with a
// further modification from [UTF-7] that "," is used instead of "/".
// Modified BASE64 MUST NOT be used to represent any printing US-ASCII
// character which can represent itself.
//
// "&" is used to shift to modified BASE64 and "-" to shift back to US-
// ASCII. All names start in US-ASCII, and MUST end in US-ASCII (that
// is, a name that ends with a Unicode 16-bit octet MUST end with a "-
// ").
QString out;
QString decodebuf;
bool decoding = false;
QTextCodec * codec = QTextCodec::codecForName("UTF16-BE");
// This is extremely unlikely but still...
if ( !codec )
return "ERROR1-" + param;
for ( int i = 0; i < param.length(); i++ )
{
// Are we already decoding?
if ( decoding )
{
if ( param[i] == '-' )
{
// Decode this string as modified UTF7, which is UTF16BE encoded in base64
QByteArray utf16data = QByteArray::fromBase64( qPrintable( decodebuf ) );
if ( utf16data.isEmpty() )
{
// Print the warning, and return an error
qWarning("Invalid IMAP UTF7 sequence: '%s' contains invalid base64 '%s' - please report", qPrintable(param), qPrintable(decodebuf) );
return "ERROR2-" + param;
}
// Decode it as UTF16
out += codec->toUnicode( utf16data );
// And reset the remaining
decodebuf.clear();
decoding = false;
}
else
{
// This is modified BASE64 where "," is used instead of "/"
if ( param[i] == ',' )
decodebuf.append( '/' );
else
decodebuf.append( param[i] );
}
}
else
{
// We are not decoding yet; & indicates possible start of decoding
if ( param[i] == '&' )
{
// this is either start of decoding or &-
if ( i + 2 < param.length() && param[i+1] == '-' )
{
// This is "&-" combination which means &, and does not start decoding
i++;
out.append( '&' );
}
else
decoding = true;
}
else
out.append( param[i] );
}
}
// The string MUST end with '-' and thus there should be nothing in the decodebuf
if ( !decodebuf.isEmpty() )
qWarning("Invalid IMAP UTF7 sequence: '%s' may be decoded incorrectly", qPrintable(param) );
return out;
}
#ifndef UTILS_H
#define UTILS_H
#include <QString>
class Utils
{
public:
// Decodes IMAP UTF7 data
static QString decodeIMAPutf7( const QString& param );
};
#endif // UTILS_H
......@@ -2,6 +2,6 @@
#define VERSION_H
static const unsigned int VERSION_MAJOR = 1;
static const unsigned int VERSION_MINOR = 4;
static const unsigned int VERSION_MINOR = 5;
#endif // VERSION_H