1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
2    Copyright (C) 2012 Peter Amidon <peter@picnicpark.org>
3 
4    This file is part of the Trojita Qt IMAP e-mail client,
5    http://trojita.flaska.net/
6 
7    This program is free software; you can redistribute it and/or
8    modify it under the terms of the GNU General Public License as
9    published by the Free Software Foundation; either version 2 of
10    the License or (at your option) version 3 or any later version
11    accepted by the membership of KDE e.V. (or its successor approved
12    by the membership of KDE e.V.), which shall act as a proxy
13    defined in Section 14 of version 3 of the license.
14 
15    This program is distributed in the hope that it will be useful,
16    but WITHOUT ANY WARRANTY; without even the implied warranty of
17    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18    GNU General Public License for more details.
19 
20    You should have received a copy of the GNU General Public License
21    along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 */
23 #include <QCoreApplication>
24 #include <QBuffer>
25 #include <QSettings>
26 #include "Composer/Submission.h"
27 #include "Composer/MessageComposer.h"
28 #include "Imap/Model/Model.h"
29 #include "Imap/Tasks/AppendTask.h"
30 #include "Imap/Tasks/GenUrlAuthTask.h"
31 #include "Imap/Tasks/UidSubmitTask.h"
32 #include "MSA/Sendmail.h"
33 #include "MSA/SMTP.h"
34 
35 namespace {
36 const int PROGRESS_MAX = 1000;
37 // We're very likely almost half-way there -- let's call it 45%
38 const int PROGRESS_SAVING_DONE = PROGRESS_MAX * 0.45;
39 // Updating flags might take roughly as much time as the URLAUTH
40 const int PROGRESS_DELIVERY_DONE = PROGRESS_MAX * 0.95;
41 
42 const int PROGRESS_DELIVERY_START_WITHOUT_SAVING = PROGRESS_MAX * 0.10;
43 const int PROGRESS_DELIVERY_START_WITH_SAVING = PROGRESS_MAX * 0.5;
44 }
45 
46 namespace Composer
47 {
48 
submissionProgressToString(const Submission::SubmissionProgress progress)49 QString submissionProgressToString(const Submission::SubmissionProgress progress)
50 {
51     switch (progress) {
52     case Submission::STATE_INIT:
53         return QStringLiteral("STATE_INIT");
54     case Submission::STATE_BUILDING_MESSAGE:
55         return QStringLiteral("STATE_BUILDING_MESSAGE");
56     case Submission::STATE_SAVING:
57         return QStringLiteral("STATE_SAVING");
58     case Submission::STATE_PREPARING_URLAUTH:
59         return QStringLiteral("STATE_PREPARING_URLAUTH");
60     case Submission::STATE_SUBMITTING:
61         return QStringLiteral("STATE_SUBMITTING");
62     case Submission::STATE_UPDATING_FLAGS:
63         return QStringLiteral("STATE_UPDATING_FLAGS");
64     case Submission::STATE_SENT:
65         return QStringLiteral("STATE_SENT");
66     case Submission::STATE_FAILED:
67         return QStringLiteral("STATE_FAILED");
68     }
69     return QStringLiteral("[unknown: %1]").arg(QString::number(static_cast<int>(progress)));
70 }
71 
Submission(QObject * parent,Imap::Mailbox::Model * model,MSA::MSAFactory * msaFactory,const QString & accountId)72 Submission::Submission(QObject *parent, Imap::Mailbox::Model *model, MSA::MSAFactory *msaFactory, const QString &accountId) :
73     QObject(parent),
74     m_appendUidReceived(false), m_appendUidValidity(0), m_appendUid(0), m_genUrlAuthReceived(false),
75     m_saveToSentFolder(false), m_useBurl(false), m_useImapSubmit(false), m_state(STATE_INIT),
76     m_msaMaximalProgress(0),
77     m_composer(0), m_model(model), m_msaFactory(msaFactory),
78     m_accountId(accountId),
79     m_updateReplyingToMessageFlagsTask(0),
80     m_updateForwardingMessageFlagsTask(0)
81 {
82     m_composer = new Composer::MessageComposer(model, this);
83     m_composer->setPreloadEnabled(shouldBuildMessageLocally());
84 }
85 
composer()86 MessageComposer *Submission::composer()
87 {
88     return m_composer;
89 }
90 
~Submission()91 Submission::~Submission()
92 {
93 }
94 
accountId() const95 QString Submission::accountId() const
96 {
97     return m_accountId;
98 }
99 
changeConnectionState(const SubmissionProgress state)100 void Submission::changeConnectionState(const SubmissionProgress state)
101 {
102     m_state = state;
103     if (m_model)
104         m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"), submissionProgressToString(m_state));
105 
106     // Now broadcast a human-readable message and update the progress dialog
107     switch (state) {
108     case STATE_INIT:
109         emit progressMin(0);
110         emit progressMax(0);
111         emit progress(0);
112         emit updateStatusMessage(tr("Preparing to send"));
113         break;
114     case STATE_BUILDING_MESSAGE:
115         emit progressMax(0);
116         emit progress(0);
117         emit updateStatusMessage(tr("Creating message"));
118         break;
119     case STATE_SAVING:
120         emit progressMax(0);
121         emit progress(0);
122         emit updateStatusMessage(tr("Saving to the sent folder"));
123         break;
124     case STATE_PREPARING_URLAUTH:
125         emit progressMax(PROGRESS_MAX);
126         emit progress(PROGRESS_SAVING_DONE);
127         emit updateStatusMessage(tr("Preparing message for delivery"));
128         break;
129     case STATE_SUBMITTING:
130         emit progressMax(PROGRESS_MAX);
131         emit progress(m_saveToSentFolder ? PROGRESS_DELIVERY_START_WITH_SAVING : PROGRESS_DELIVERY_START_WITHOUT_SAVING);
132         emit updateStatusMessage(tr("Submitting message"));
133         break;
134     case STATE_UPDATING_FLAGS:
135         emit progressMax(PROGRESS_MAX);
136         emit progress(PROGRESS_DELIVERY_DONE);
137         emit updateStatusMessage(tr("Updating message keywords"));
138         break;
139     case STATE_SENT:
140         emit progressMax(PROGRESS_MAX);
141         emit progress(PROGRESS_MAX);
142         emit updateStatusMessage(tr("Message sent"));
143         break;
144     case STATE_FAILED:
145         // revert to the busy indicator
146         emit progressMin(0);
147         emit progressMax(0);
148         emit progress(0);
149         emit updateStatusMessage(tr("Sending failed"));
150         break;
151     }
152 }
153 
setImapOptions(const bool saveToSentFolder,const QString & sentFolderName,const QString & hostname,const QString & username,const bool useImapSubmit)154 void Submission::setImapOptions(const bool saveToSentFolder, const QString &sentFolderName,
155                                 const QString &hostname, const QString &username, const bool useImapSubmit)
156 {
157     m_saveToSentFolder = saveToSentFolder;
158     m_sentFolderName = sentFolderName;
159     m_imapHostname = hostname;
160     m_imapUsername = username;
161     m_useImapSubmit = useImapSubmit;
162     m_composer->setPreloadEnabled(shouldBuildMessageLocally());
163 }
164 
setSmtpOptions(const bool useBurl,const QString & smtpUsername)165 void Submission::setSmtpOptions(const bool useBurl, const QString &smtpUsername)
166 {
167     m_useBurl = useBurl;
168     if (m_useBurl && !m_model->isGenUrlAuthSupported()) {
169         m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"), QStringLiteral("Cannot BURL without the URLAUTH extension"));
170         m_useBurl = false;
171     }
172     m_smtpUsername = smtpUsername;
173     m_composer->setPreloadEnabled(shouldBuildMessageLocally());
174 }
175 
send()176 void Submission::send()
177 {
178     if (!m_model) {
179         gotError(tr("The IMAP connection has disappeared. "
180                     "You'll have close the composer, save the draft and re-open it later. "
181                     "The attachments will have to be added later. Sorry for the trouble, "
182                     "please see <a href=\"https://projects.flaska.net/issues/640\">https://projects.flaska.net/issues/640</a> "
183                     "for details."));
184         return;
185     }
186 
187     // this double-updating is needed in case the same Submission attempts to send a message more than once
188     changeConnectionState(STATE_INIT);
189     changeConnectionState(STATE_BUILDING_MESSAGE);
190 
191     if (shouldBuildMessageLocally() && !m_composer->isReadyForSerialization()) {
192         // we have to wait until the data arrive
193         // FIXME: relax this to wait here
194         gotError(tr("Some data are not available yet"));
195     } else {
196         slotMessageDataAvailable();
197     }
198 }
199 
200 
slotMessageDataAvailable()201 void Submission::slotMessageDataAvailable()
202 {
203     m_rawMessageData.clear();
204     QBuffer buf(&m_rawMessageData);
205     buf.open(QIODevice::WriteOnly);
206     QString errorMessage;
207     QList<Imap::Mailbox::CatenatePair> catenateable;
208 
209     if (shouldBuildMessageLocally() && !m_composer->asRawMessage(&buf, &errorMessage)) {
210         gotError(tr("Cannot send right now -- saving failed:\n %1").arg(errorMessage));
211         return;
212     }
213     if (m_model->isCatenateSupported() && !m_composer->asCatenateData(catenateable, &errorMessage)) {
214         gotError(tr("Cannot send right now -- saving (CATENATE) failed:\n %1").arg(errorMessage));
215         return;
216     }
217 
218     if (m_saveToSentFolder) {
219         Q_ASSERT(m_model);
220         m_appendUidReceived = false;
221         m_genUrlAuthReceived = false;
222 
223         changeConnectionState(STATE_SAVING);
224         QPointer<Imap::Mailbox::AppendTask> appendTask = 0;
225 
226         if (m_model->isCatenateSupported()) {
227             // FIXME: without UIDPLUS, there isn't much point in $SubmitPending...
228             appendTask = QPointer<Imap::Mailbox::AppendTask>(
229                         m_model->appendIntoMailbox(
230                             m_sentFolderName,
231                             catenateable,
232                             QStringList() << QStringLiteral("\\Seen"),
233                             m_composer->timestamp()));
234         } else {
235             // FIXME: without UIDPLUS, there isn't much point in $SubmitPending...
236             appendTask = QPointer<Imap::Mailbox::AppendTask>(
237                         m_model->appendIntoMailbox(
238                             m_sentFolderName,
239                             m_rawMessageData,
240                             QStringList() << QStringLiteral("\\Seen"),
241                             m_composer->timestamp()));
242         }
243 
244         Q_ASSERT(appendTask);
245         connect(appendTask.data(), &Imap::Mailbox::AppendTask::appendUid, this, &Submission::slotAppendUidKnown);
246         connect(appendTask.data(), &Imap::Mailbox::ImapTask::completed, this, &Submission::slotAppendSucceeded);
247         connect(appendTask.data(), &Imap::Mailbox::ImapTask::failed, this, &Submission::slotAppendFailed);
248     } else {
249         slotInvokeMsaNow();
250     }
251 }
252 
slotAskForUrl()253 void Submission::slotAskForUrl()
254 {
255     Q_ASSERT(m_appendUidReceived && m_useBurl);
256     changeConnectionState(STATE_PREPARING_URLAUTH);
257     Imap::Mailbox::GenUrlAuthTask *genUrlAuthTask = QPointer<Imap::Mailbox::GenUrlAuthTask>(
258                 m_model->generateUrlAuthForMessage(m_imapHostname,
259                                                    killDomainPartFromString(m_imapUsername),
260                                                    m_sentFolderName,
261                                                    m_appendUidValidity, m_appendUid, QString(),
262                                                    QStringLiteral("submit+%1").arg(
263                                                        killDomainPartFromString(m_smtpUsername))
264                                                    ));
265     connect(genUrlAuthTask, &Imap::Mailbox::GenUrlAuthTask::gotAuth, this, &Submission::slotGenUrlAuthReceived);
266     connect(genUrlAuthTask, &Imap::Mailbox::ImapTask::failed, this, &Submission::gotError);
267 }
268 
slotInvokeMsaNow()269 void Submission::slotInvokeMsaNow()
270 {
271     changeConnectionState(STATE_SUBMITTING);
272     MSA::AbstractMSA *msa = m_msaFactory->create(this);
273     connect(msa, &MSA::AbstractMSA::progressMax, this, &Submission::onMsaProgressMaxChanged);
274     connect(msa, &MSA::AbstractMSA::progress, this, &Submission::onMsaProgressCurrentChanged);
275     connect(msa, &MSA::AbstractMSA::sent, this, &Submission::sent);
276     connect(msa, &MSA::AbstractMSA::error, this, &Submission::gotError);
277     connect(msa, &MSA::AbstractMSA::passwordRequested, this, &Submission::passwordRequested);
278     connect(this, &Submission::gotPassword, msa, &MSA::AbstractMSA::setPassword);
279     connect(this, &Submission::canceled, msa, &MSA::AbstractMSA::cancel);
280 
281     if (m_useImapSubmit && msa->supportsImapSending() && m_appendUidReceived) {
282         Imap::Mailbox::UidSubmitOptionsList options;
283         options.append(qMakePair<QByteArray,QVariant>("FROM", m_composer->rawFromAddress()));
284         Q_FOREACH(const QByteArray &recipient, m_composer->rawRecipientAddresses()) {
285             options.append(qMakePair<QByteArray,QVariant>("RECIPIENT", recipient));
286         }
287         msa->sendImap(m_sentFolderName, m_appendUidValidity, m_appendUid, options);
288     } else if (m_genUrlAuthReceived && m_useBurl) {
289         msa->sendBurl(m_composer->rawFromAddress(), m_composer->rawRecipientAddresses(), m_urlauth.toUtf8());
290     } else {
291         msa->sendMail(m_composer->rawFromAddress(), m_composer->rawRecipientAddresses(), m_rawMessageData);
292     }
293 }
294 
setPassword(const QString & password)295 void Submission::setPassword(const QString &password)
296 {
297     emit gotPassword(password);
298 }
299 
cancelPassword()300 void Submission::cancelPassword()
301 {
302     emit canceled();
303 }
304 
gotError(const QString & error)305 void Submission::gotError(const QString &error)
306 {
307     if (m_model)
308         m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"), QStringLiteral("gotError: %1").arg(error));
309     changeConnectionState(STATE_FAILED);
310     emit failed(error);
311 }
312 
sent()313 void Submission::sent()
314 {
315     if (m_composer->replyingToMessage().isValid()) {
316         m_updateReplyingToMessageFlagsTask = m_model->setMessageFlags(QModelIndexList() << m_composer->replyingToMessage(),
317                                                                       QStringLiteral("\\Answered"), Imap::Mailbox::FLAG_ADD);
318         connect(m_updateReplyingToMessageFlagsTask, &Imap::Mailbox::ImapTask::completed,
319                 this, &Submission::onUpdatingFlagsOfReplyingToSucceded);
320         connect(m_updateReplyingToMessageFlagsTask, &Imap::Mailbox::ImapTask::failed,
321                 this, &Submission::onUpdatingFlagsOfReplyingToFailed);
322         changeConnectionState(STATE_UPDATING_FLAGS);
323     } else if (m_composer->forwardingMessage().isValid()) {
324         m_updateForwardingMessageFlagsTask = m_model->setMessageFlags(QModelIndexList() << m_composer->forwardingMessage(),
325                                                                       QStringLiteral("$Forwarded"), Imap::Mailbox::FLAG_ADD);
326         connect(m_updateForwardingMessageFlagsTask, &Imap::Mailbox::ImapTask::completed,
327                 this, &Submission::onUpdatingFlagsOfForwardingSucceeded);
328         connect(m_updateForwardingMessageFlagsTask, &Imap::Mailbox::ImapTask::failed,
329                 this, &Submission::onUpdatingFlagsOfForwardingFailed);
330         changeConnectionState(STATE_UPDATING_FLAGS);
331     } else {
332         changeConnectionState(STATE_SENT);
333         emit succeeded();
334     }
335 #if 0
336     if (m_appendUidReceived) {
337         // FIXME: check the UIDVALIDITY!!!
338         // FIXME: doesn't work at all; the messageIndexByUid() only works on already selected mailboxes
339         QModelIndex message = m_mainWindow->imapModel()->
340                 messageIndexByUid(QSettings().value(Common::SettingsNames::composerImapSentKey, tr("Sent")).toString(), m_appendUid);
341         if (message.isValid()) {
342             m_mainWindow->imapModel()->setMessageFlags(QModelIndexList() << message,
343                                                        QLatin1String("\\Seen $Submitted"), Imap::Mailbox::FLAG_USE_THESE);
344         }
345     }
346 #endif
347 
348     // FIXME: move back to the currently selected mailbox
349 }
350 
351 /** @short Remember the APPENDUID as reported by the APPEND operation */
slotAppendUidKnown(const uint uidValidity,const uint uid)352 void Submission::slotAppendUidKnown(const uint uidValidity, const uint uid)
353 {
354     m_appendUidValidity = uidValidity;
355     m_appendUid = uid;
356 }
357 
slotAppendFailed(const QString & error)358 void Submission::slotAppendFailed(const QString &error)
359 {
360     gotError(tr("APPEND failed: %1").arg(error));
361 }
362 
slotAppendSucceeded()363 void Submission::slotAppendSucceeded()
364 {
365     if (m_appendUid && m_appendUidValidity) {
366         // Only ever consider valid UIDVALIDITY/UID pair
367         m_appendUidReceived = true;
368 
369         if (m_useBurl) {
370             slotAskForUrl();
371         } else {
372             slotInvokeMsaNow();
373         }
374     } else {
375         m_useBurl = false;
376         m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"),
377                           QStringLiteral("APPEND does not contain APPENDUID or UIDVALIDITY, cannot use BURL or the SUBMIT command"));
378         slotInvokeMsaNow();
379     }
380 }
381 
382 /** @short Remember the GENURLAUTH response */
slotGenUrlAuthReceived(const QString & url)383 void Submission::slotGenUrlAuthReceived(const QString &url)
384 {
385     m_urlauth = url;
386     if (!m_urlauth.isEmpty()) {
387         m_genUrlAuthReceived = true;
388         slotInvokeMsaNow();
389     } else {
390         gotError(tr("The URLAUTH response does not contain a proper URL"));
391     }
392 }
393 
394 /** @short Remove the "@domain" from a string */
killDomainPartFromString(const QString & s)395 QString Submission::killDomainPartFromString(const QString &s)
396 {
397     return s.split(QLatin1Char('@'))[0];
398 }
399 
400 /** @short Return true if the message payload shall be built locally */
shouldBuildMessageLocally() const401 bool Submission::shouldBuildMessageLocally() const
402 {
403     if (!m_useImapSubmit) {
404         // sending via SMTP or Sendmail
405         // Unless all of URLAUTH, CATENATE and BURL is present and enabled, we will still have to download the data in the end
406         return ! (m_useBurl && m_model->isCatenateSupported() && m_model->isGenUrlAuthSupported());
407     } else {
408         return ! m_model->isCatenateSupported();
409     }
410 }
411 
onUpdatingFlagsOfReplyingToSucceded()412 void Submission::onUpdatingFlagsOfReplyingToSucceded()
413 {
414     m_updateReplyingToMessageFlagsTask = 0;
415     changeConnectionState(STATE_SENT);
416     emit succeeded();
417 }
418 
onUpdatingFlagsOfReplyingToFailed()419 void Submission::onUpdatingFlagsOfReplyingToFailed()
420 {
421     m_updateReplyingToMessageFlagsTask = 0;
422     m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"),
423                       QStringLiteral("Cannot update flags of the message we replied to -- interesting, but we cannot do anything at this point anyway"));
424     changeConnectionState(STATE_SENT);
425     emit succeeded();
426 }
427 
onUpdatingFlagsOfForwardingSucceeded()428 void Submission::onUpdatingFlagsOfForwardingSucceeded()
429 {
430     m_updateForwardingMessageFlagsTask = 0;
431     changeConnectionState(STATE_SENT);
432     emit succeeded();
433 }
434 
onUpdatingFlagsOfForwardingFailed()435 void Submission::onUpdatingFlagsOfForwardingFailed()
436 {
437     m_updateForwardingMessageFlagsTask = 0;
438     m_model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Submission"),
439                       QStringLiteral("Cannot update flags of the message we forwarded -- interesting, but we cannot do anything at this point anyway"));
440     changeConnectionState(STATE_SENT);
441     emit succeeded();
442 }
443 
onMsaProgressCurrentChanged(const int value)444 void Submission::onMsaProgressCurrentChanged(const int value)
445 {
446     if (m_msaMaximalProgress > 0) {
447         // prevent division by zero or performing operations which do not make any sense
448         int low = m_saveToSentFolder ? PROGRESS_DELIVERY_START_WITH_SAVING : PROGRESS_DELIVERY_START_WITHOUT_SAVING;
449         int high = PROGRESS_DELIVERY_DONE;
450         emit progress(1.0 * value / m_msaMaximalProgress * (high - low) + low);
451     }
452 }
453 
onMsaProgressMaxChanged(const int max)454 void Submission::onMsaProgressMaxChanged(const int max)
455 {
456     m_msaMaximalProgress = max;
457 }
458 
459 }
460 
461