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