1 #include <QtCore/QLibraryInfo>
2 #include <QtCore/QStandardPaths>
3 #include <QtCore/QThread>
4 
5 #include "birdtrayapp.h"
6 #ifdef Q_OS_WIN
7 #  include "birdtrayeventfilter.h"
8 #endif /* Q_OS_WIN */
9 #include "utils.h"
10 #include "morkparser.h"
11 #include "windowtools.h"
12 #include "version.h"
13 #include "log.h"
14 
15 #define SINGLE_INSTANCE_SERVER_NAME "birdtray.ulduzsoft.single.instance.server.socket"
16 #define TOGGLE_THUNDERBIRD_COMMAND "toggle-tb"
17 #define SHOW_THUNDERBIRD_COMMAND "show-tb"
18 #define HIDE_THUNDERBIRD_COMMAND "hide-tb"
19 #define SETTINGS_COMMAND "settings"
20 
21 
BirdtrayApp(int & argc,char ** argv)22 BirdtrayApp::BirdtrayApp(int &argc, char** argv) : QApplication(argc, argv)
23 {
24     QApplication::setWindowIcon(QIcon(QString::fromUtf8(":/res/birdtray.ico")));
25     QCoreApplication::setOrganizationName("ulduzsoft");
26     QCoreApplication::setOrganizationDomain("ulduzsoft.com");
27     QCoreApplication::setApplicationName("birdtray");
28     QCoreApplication::setApplicationVersion(Utils::getBirdtrayVersion());
29     // This prevents exiting the application when the dialogs are closed on Gnome/XFCE
30     QApplication::setQuitOnLastWindowClosed(false);
31 
32     bool translationLoadedSuccessfully = loadTranslations();
33     QCoreApplication::installTranslator(&qtTranslator);
34     QCoreApplication::installTranslator(&dynamicTranslator);
35     QCoreApplication::installTranslator(&mainTranslator);
36 
37 #ifdef Q_OS_WIN
38     BirdtrayEventFilter filter;
39     installNativeEventFilter(&filter);
40 #endif /* Q_OS_WIN */
41 
42     parseCmdArguments();
43 
44     QString morkPath = commandLineParser.value("dump-mork");
45     if (!morkPath.isEmpty()) {
46         QTimer::singleShot(0, [=]() { exit(MorkParser::dumpMorkFile(morkPath)); });
47         return;
48     }
49     QString imapString = commandLineParser.value("decode");
50     if (!imapString.isEmpty()) {
51         printf("Decoded: %s\n", qPrintable(Utils::decodeIMAPutf7(imapString)));
52         QTimer::singleShot(0, &BirdtrayApp::quit);
53         return;
54     }
55 
56     if (!startSingleInstanceServer()) {
57         QTimer::singleShot(0, &BirdtrayApp::quit);
58         return;
59     }
60 
61     Log::initialize(commandLineParser.value("log"));
62 
63     Log::debug( "Birdtray version %d.%d.%d started", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH );
64 
65     ensureSystemTrayAvailable();
66     // Load settings
67     settings = new Settings();
68     if (commandLineParser.isSet("reset-settings")) {
69         settings->save(); // Saving without loading will reset the values
70     } else {
71         settings->load();
72     }
73 
74     if (!translationLoadedSuccessfully) {
75         Log::debug("Failed to load translation for %s", qPrintable(QLocale::system().name()));
76     }
77     autoUpdater = new AutoUpdater();
78     trayIcon = new TrayIcon(commandLineParser.isSet("settings"));
79 }
80 
~BirdtrayApp()81 BirdtrayApp::~BirdtrayApp() {
82     if (singleInstanceServer != nullptr) {
83         singleInstanceServer->close();
84         singleInstanceServer->deleteLater();
85         singleInstanceServer = nullptr;
86     }
87     delete trayIcon;
88     delete autoUpdater;
89     delete settings;
90 }
91 
get()92 BirdtrayApp* BirdtrayApp::get() {
93     return dynamic_cast<BirdtrayApp*>(QCoreApplication::instance());
94 }
95 
getSettings() const96 Settings* BirdtrayApp::getSettings() const {
97     return settings;
98 }
99 
getAutoUpdater() const100 AutoUpdater* BirdtrayApp::getAutoUpdater() const {
101     return autoUpdater;
102 }
103 
getTrayIcon() const104 TrayIcon* BirdtrayApp::getTrayIcon() const {
105     return trayIcon;
106 }
107 
event(QEvent * event)108 bool BirdtrayApp::event(QEvent* event) {
109     if (event->type() == QEvent::LocaleChange) {
110         if (!loadTranslations()) {
111             Log::debug("Failed to load translation for %s", qPrintable(QLocale::system().name()));
112         }
113         return true;
114     }
115     return QApplication::event(event);
116 }
117 
onSecondInstanceAttached()118 void BirdtrayApp::onSecondInstanceAttached() {
119     QLocalSocket* clientSocket = singleInstanceServer->nextPendingConnection();
120     if (clientSocket != nullptr) {
121         connect(clientSocket, &QLocalSocket::readyRead,
122                 this, [=]() { onSecondInstanceCommand(clientSocket); });
123     }
124 }
125 
loadTranslations()126 bool BirdtrayApp::loadTranslations() {
127     QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
128     locations.prepend(QCoreApplication::applicationDirPath());
129     std::transform(locations.begin(), locations.end(), locations.begin(),
130             [](QString path) { return path.append("/translations"); });
131     QLocale locale = QLocale::system();
132     bool success = loadTranslation(
133             qtTranslator, locale, "qt", {QLibraryInfo::location(QLibraryInfo::TranslationsPath)});
134     success &= loadTranslation(dynamicTranslator, locale, "dynamic", locations);
135     success &= loadTranslation(mainTranslator, locale, "main", locations);
136     return success;
137 }
138 
loadTranslation(QTranslator & translator,QLocale & locale,const QString & translationName,const QStringList & paths)139 bool BirdtrayApp::loadTranslation(QTranslator &translator, QLocale &locale,
140                                   const QString &translationName, const QStringList &paths) {
141     QLocale languageWithoutCountry(locale.language());
142     for (const QString &path : paths) {
143         if (translator.load(locale, translationName, "_", path)) {
144             return true;
145         }
146         // On Ubuntu, when switching to another language, the LANGUAGE environment variable
147         // does not include the base language, only the language with country,
148         // e.g. LANGUAGE=de_DE:en_US:en
149         // instead of LANGUAGE=de_DE:de:en_US:en
150         // As a result, the translator does not find the <translationName>_de.qm files.
151         // That's why we try to load the translation without the country appendix.
152         if (translator.load(languageWithoutCountry, translationName, "_", path)) {
153             return true;
154         }
155     }
156     return false;
157 }
158 
parseCmdArguments()159 void BirdtrayApp::parseCmdArguments() {
160     commandLineParser.setApplicationDescription(
161             tr("A free system tray notification for new mail for Thunderbird"));
162     commandLineParser.addHelpOption();
163     commandLineParser.addVersionOption();
164     commandLineParser.addOptions({
165             {"dump-mork", tr("Display the contents of the given mork database."),
166              tr("databaseFile")},
167             {"decode", tr("Decode an IMAP Utf7 string."), tr("string")},
168             {SETTINGS_COMMAND, tr("Show the settings.")},
169             {{"t", TOGGLE_THUNDERBIRD_COMMAND}, tr("Toggle the Thunderbird window.")},
170             {{"s", SHOW_THUNDERBIRD_COMMAND}, tr("Show the Thunderbird window.")},
171             {{"H", HIDE_THUNDERBIRD_COMMAND}, tr("Hide the Thunderbird window.")},
172             {{"r", "reset-settings"}, tr("Reset the settings to the defaults.")},
173             {{"l", "log"}, tr("Write log to a file."), tr("FILE")}
174     });
175     commandLineParser.process(*this);
176 }
177 
startSingleInstanceServer()178 bool BirdtrayApp::startSingleInstanceServer() {
179     singleInstanceServer = new QLocalServer();
180     bool serverListening = singleInstanceServer->listen(SINGLE_INSTANCE_SERVER_NAME);
181     if (!serverListening
182         && (singleInstanceServer->serverError() == QAbstractSocket::AddressInUseError)) {
183         if (connectToRunningInstance()) {
184             return false;
185         }
186         // The other instance might have crashed, try to remove the dead socket and try again.
187         QLocalServer::removeServer(SINGLE_INSTANCE_SERVER_NAME);
188         serverListening = singleInstanceServer->listen(SINGLE_INSTANCE_SERVER_NAME);
189     }
190 
191 #if defined(Q_OS_WIN)
192     // On Windows, binding to the same address as the other instance doesn't fail,
193     // so we use a mutex to detect if there is another Birdtray instance.
194     if (serverListening) {
195         CreateMutex(nullptr, true, TEXT(SINGLE_INSTANCE_SERVER_NAME));
196         if (GetLastError() == ERROR_ALREADY_EXISTS) {
197             singleInstanceServer->close(); // Disable our new server instance
198             serverListening = false;
199         }
200     }
201 #endif
202     if (!serverListening) {
203         return !connectToRunningInstance();
204     }
205     connect(singleInstanceServer, &QLocalServer::newConnection,
206             this, &BirdtrayApp::onSecondInstanceAttached);
207     return true;
208 }
209 
connectToRunningInstance() const210 bool BirdtrayApp::connectToRunningInstance() const {
211     bool connectionSuccessful = false;
212     QLocalSocket serverSocket;
213     serverSocket.connectToServer(SINGLE_INSTANCE_SERVER_NAME, QLocalSocket::WriteOnly);
214     if (serverSocket.waitForConnected()) {
215         sendCommandsToRunningInstance(serverSocket);
216         connectionSuccessful = true;
217         serverSocket.disconnectFromServer();
218     }
219     return connectionSuccessful;
220 }
221 
sendCommandsToRunningInstance(QLocalSocket & serverSocket) const222 void BirdtrayApp::sendCommandsToRunningInstance(QLocalSocket &serverSocket) const {
223     if (commandLineParser.isSet(TOGGLE_THUNDERBIRD_COMMAND)) {
224         serverSocket.write(TOGGLE_THUNDERBIRD_COMMAND "\n");
225     }
226     if (commandLineParser.isSet(SHOW_THUNDERBIRD_COMMAND)) {
227         serverSocket.write(SHOW_THUNDERBIRD_COMMAND "\n");
228     }
229     if (commandLineParser.isSet(HIDE_THUNDERBIRD_COMMAND)) {
230         serverSocket.write(HIDE_THUNDERBIRD_COMMAND "\n");
231     }
232     if (commandLineParser.isSet(SETTINGS_COMMAND)) {
233         serverSocket.write(SETTINGS_COMMAND "\n");
234     }
235     serverSocket.waitForBytesWritten();
236 }
237 
onSecondInstanceCommand(QLocalSocket * clientSocket)238 void BirdtrayApp::onSecondInstanceCommand(QLocalSocket* clientSocket) {
239     if (!clientSocket->canReadLine()) {
240         return;
241     }
242     QByteArray line = clientSocket->readLine(128);
243     line.chop(1);
244     if (line == TOGGLE_THUNDERBIRD_COMMAND) {
245         if (trayIcon->getWindowTools()->isHidden()) {
246             trayIcon->showThunderbird();
247         } else {
248             trayIcon->hideThunderbird();
249         }
250     } else if (line == SHOW_THUNDERBIRD_COMMAND) {
251         trayIcon->showThunderbird();
252     } else if (line == HIDE_THUNDERBIRD_COMMAND) {
253         trayIcon->hideThunderbird();
254     } else if (line == SETTINGS_COMMAND) {
255         trayIcon->showSettings();
256     }
257 }
258 
ensureSystemTrayAvailable()259 void BirdtrayApp::ensureSystemTrayAvailable() {
260     int passed = 0;
261     while (!QSystemTrayIcon::isSystemTrayAvailable()) {
262         if (passed == 0) {
263             qDebug("Waiting for system tray to become available");
264         }
265         passed++;
266         if (passed > 120) {
267             Log::fatal( QApplication::tr("Sorry, system tray cannot be controlled "
268                                           "through this add-on on your operating system.") );
269         }
270         QThread::msleep(500);
271     }
272 }
273