1 /*
2 filtersylpheed.cpp - Sylpheed maildir mail import
3
4 SPDX-FileCopyrightText: 2005 Danny Kukawka <danny.kukawka@web.de>
5 SPDX-FileCopyrightText: 2012-2021 Laurent Montel <montel@kde.org>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10 #include "filtersylpheed.h"
11
12 #include "mailimporter_debug.h"
13 #include <KLocalizedString>
14 #include <QDomDocument>
15 #include <QDomElement>
16 #include <QFileDialog>
17
18 using namespace MailImporter;
19
20 class MailImporter::FilterSylpheedPrivate
21 {
22 public:
23 int mImportDirDone = 0;
24 int mTotalDir = 0;
25 };
26 /** Default constructor. */
FilterSylpheed()27 FilterSylpheed::FilterSylpheed()
28 : Filter(i18n("Import Sylpheed Maildirs and Folder Structure"),
29 QStringLiteral("Danny Kukawka"),
30 i18n("<p><b>Sylpheed import filter</b></p>"
31 "<p>Select the base directory of the Sylpheed mailfolder you want to import "
32 "(usually: ~/Mail ).</p>"
33 "<p>Since it is possible to recreate the folder structure, the folders "
34 "will be stored under: \"Sylpheed-Import\" in your local folder.</p>"
35 "<p>This filter also recreates the status of message, e.g. new or forwarded.</p>"))
36 , d(new MailImporter::FilterSylpheedPrivate)
37 {
38 }
39
40 /** Destructor. */
41 FilterSylpheed::~FilterSylpheed() = default;
42
isMailerFound()43 QString FilterSylpheed::isMailerFound()
44 {
45 QDir directory(FilterSylpheed::defaultSettingsPath());
46 if (directory.exists()) {
47 return i18nc("name of sylpheed application", "Sylpheed");
48 }
49 return {};
50 }
51
defaultSettingsPath()52 QString FilterSylpheed::defaultSettingsPath()
53 {
54 return QDir::homePath() + QLatin1String("/.sylpheed-2.0/");
55 }
56
localMailDirPath()57 QString FilterSylpheed::localMailDirPath()
58 {
59 QFile folderListFile(FilterSylpheed::defaultSettingsPath() + QLatin1String("/folderlist.xml"));
60 if (folderListFile.exists()) {
61 QDomDocument doc;
62 QString errorMsg;
63 int errorRow;
64 int errorCol;
65 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
66 if (!folderListFile.open(QIODevice::ReadOnly)) {
67 qCWarning(MAILIMPORTER_LOG) << "Impossible to open " << folderListFile.fileName();
68 }
69 #endif
70 if (!doc.setContent(&folderListFile, &errorMsg, &errorRow, &errorCol)) {
71 qCDebug(MAILIMPORTER_LOG) << "Unable to load document.Parse error in line " << errorRow << ", col " << errorCol << ": " << errorMsg;
72 return QString();
73 }
74 QDomElement settings = doc.documentElement();
75
76 if (settings.isNull()) {
77 return QString();
78 }
79
80 for (QDomElement e = settings.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
81 if (e.tagName() == QLatin1String("folder")) {
82 if (e.hasAttribute(QStringLiteral("type"))) {
83 if (e.attribute(QStringLiteral("type")) == QLatin1String("mh")) {
84 return e.attribute(QStringLiteral("path"));
85 }
86 }
87 }
88 }
89 }
90 return QString();
91 }
92
93 /** Recursive import of Sylpheed maildir. */
import()94 void FilterSylpheed::import()
95 {
96 QString homeDir = localMailDirPath();
97 if (homeDir.isEmpty()) {
98 homeDir = QDir::homePath();
99 }
100 // Select directory from where I have to import files
101 const QString maildir = QFileDialog::getExistingDirectory(nullptr, QString(), homeDir);
102 if (!maildir.isEmpty()) {
103 importMails(maildir);
104 }
105 }
106
processDirectory(const QString & path)107 void FilterSylpheed::processDirectory(const QString &path)
108 {
109 QDir dir(path);
110 const QStringList rootSubDirs = dir.entryList(QStringList(QStringLiteral("[^\\.]*")), QDir::Dirs, QDir::Name);
111 QStringList::ConstIterator end = rootSubDirs.constEnd();
112 for (QStringList::ConstIterator filename = rootSubDirs.constBegin(); filename != end; ++filename) {
113 if (filterInfo()->shouldTerminate()) {
114 break;
115 }
116 importDirContents(dir.filePath(*filename));
117 filterInfo()->setOverall((d->mTotalDir > 0) ? (int)((float)d->mImportDirDone / d->mTotalDir * 100) : 0);
118 ++d->mImportDirDone;
119 }
120 }
121
importMails(const QString & maildir)122 void FilterSylpheed::importMails(const QString &maildir)
123 {
124 if (maildir.isEmpty()) {
125 filterInfo()->alert(i18n("No directory selected."));
126 return;
127 }
128 setMailDir(maildir);
129 /**
130 * If the user only select homedir no import needed because
131 * there should be no files and we surely import wrong files.
132 */
133 if (mailDir() == QDir::homePath() || mailDir() == (QDir::homePath() + QLatin1Char('/'))) {
134 filterInfo()->addErrorLogEntry(i18n("No files found for import."));
135 } else {
136 filterInfo()->setOverall(0);
137
138 d->mImportDirDone = 0;
139
140 /** Recursive import of the MailFolders */
141 QDir dir(mailDir());
142
143 d->mTotalDir = Filter::countDirectory(dir, false);
144 processDirectory(mailDir());
145
146 filterInfo()->addInfoLogEntry(i18n("Finished importing emails from %1", mailDir()));
147 if (countDuplicates() > 0) {
148 filterInfo()->addInfoLogEntry(i18np("1 duplicate message not imported", "%1 duplicate messages not imported", countDuplicates()));
149 }
150 }
151 if (filterInfo()->shouldTerminate()) {
152 filterInfo()->addInfoLogEntry(i18n("Finished import, canceled by user."));
153 }
154 clearCountDuplicate();
155 filterInfo()->setCurrent(100);
156 filterInfo()->setOverall(100);
157 }
158
159 /**
160 * Import of a directory contents.
161 * @param info Information storage for the operation.
162 * @param dirName The name of the directory to import.
163 */
importDirContents(const QString & dirName)164 void FilterSylpheed::importDirContents(const QString &dirName)
165 {
166 if (filterInfo()->shouldTerminate()) {
167 return;
168 }
169
170 /** Here Import all archives in the current dir */
171 importFiles(dirName);
172
173 /** If there are subfolders, we import them one by one */
174 processDirectory(dirName);
175 }
176
excludeFile(const QString & file)177 bool FilterSylpheed::excludeFile(const QString &file)
178 {
179 if (file.endsWith(QLatin1String(".sylpheed_cache")) || file.endsWith(QLatin1String(".sylpheed_mark")) || file.endsWith(QLatin1String(".mh_sequences"))) {
180 return true;
181 }
182 return false;
183 }
184
defaultInstallFolder() const185 QString FilterSylpheed::defaultInstallFolder() const
186 {
187 return i18nc("define folder name where we will import sylpheed mails", "Sylpheed-Import") + QLatin1Char('/');
188 }
189
markFile() const190 QString FilterSylpheed::markFile() const
191 {
192 return QStringLiteral(".sylpheed_mark");
193 }
194
195 /**
196 * Import the files within a Folder.
197 * @param info Information storage for the operation.
198 * @param dirName The name of the directory to import.
199 */
importFiles(const QString & dirName)200 void FilterSylpheed::importFiles(const QString &dirName)
201 {
202 QDir dir(dirName);
203 QString _path;
204 bool generatedPath = false;
205
206 QHash<QString, unsigned long> msgflags;
207
208 QDir importDir(dirName);
209 const QString defaultInstallPath = defaultInstallFolder();
210
211 const QStringList files = importDir.entryList(QStringList(QStringLiteral("[^\\.]*")), QDir::Files, QDir::Name);
212 int currentFile = 1;
213 int numFiles = files.size();
214
215 readMarkFile(dir.filePath(markFile()), msgflags);
216
217 QStringList::ConstIterator end(files.constEnd());
218 for (QStringList::ConstIterator mailFile = files.constBegin(); mailFile != end; ++mailFile, ++currentFile) {
219 if (filterInfo()->shouldTerminate()) {
220 return;
221 }
222 QString _mfile = *mailFile;
223 if (!excludeFile(_mfile)) {
224 if (!generatedPath) {
225 _path = defaultInstallPath;
226 QString _tmp = dir.filePath(*mailFile);
227 _tmp.remove(_tmp.length() - _mfile.length() - 1, _mfile.length() + 1);
228 _path += _tmp.remove(mailDir(), Qt::CaseSensitive);
229 QString _info = _path;
230 filterInfo()->addInfoLogEntry(i18n("Import folder %1...", _info.remove(0, 15)));
231
232 filterInfo()->setFrom(_info);
233 filterInfo()->setTo(_path);
234 generatedPath = true;
235 }
236
237 MailImporter::MessageStatus status;
238 if (msgflags[_mfile]) {
239 status = msgFlagsToString((msgflags[_mfile]));
240 } else {
241 status.setRead(true); // 0 == read
242 }
243 if (!importMessage(_path, dir.filePath(*mailFile), filterInfo()->removeDupMessage(), status)) {
244 filterInfo()->addErrorLogEntry(i18n("Could not import %1", *mailFile));
245 }
246 filterInfo()->setCurrent((int)((float)currentFile / numFiles * 100));
247 }
248 }
249 }
250
readMarkFile(const QString & path,QHash<QString,unsigned long> & dict)251 void FilterSylpheed::readMarkFile(const QString &path, QHash<QString, unsigned long> &dict)
252 {
253 /* Each sylpheed mail directory contains a .sylpheed_mark file which
254 * contains all the flags for each messages. The layout of this file
255 * is documented in the source code of sylpheed: in procmsg.h for
256 * the flag bits, and procmsg.c.
257 *
258 * Note that the mark file stores 32 bit unsigned integers in the
259 * platform's native "endianness".
260 *
261 * The mark file starts with a 32 bit unsigned integer with a version
262 * number. It is then followed by pairs of 32 bit unsigned integers,
263 * the first one with the message file name (which is a number),
264 * and the second one with the actual message flags */
265
266 quint32 in;
267 quint32 flags;
268 QFile file(path);
269
270 if (!file.open(QIODevice::ReadOnly)) {
271 return;
272 }
273
274 QDataStream stream(&file);
275
276 if (Q_BYTE_ORDER == Q_LITTLE_ENDIAN) {
277 stream.setByteOrder(QDataStream::LittleEndian);
278 }
279
280 /* Read version; if the value is reasonably too big, we're looking
281 * at a file created on another platform. I don't have any test
282 * marks/folders, so just ignoring this case */
283 stream >> in;
284 if (in > (quint32)0xffff) {
285 return;
286 }
287
288 while (!stream.atEnd()) {
289 if (filterInfo()->shouldTerminate()) {
290 file.close();
291 return;
292 }
293 stream >> in;
294 stream >> flags;
295 QString s;
296 s.setNum((uint)in);
297 dict.insert(s, flags);
298 }
299 }
300
msgFlagsToString(unsigned long flags)301 MailImporter::MessageStatus FilterSylpheed::msgFlagsToString(unsigned long flags)
302 {
303 MailImporter::MessageStatus status;
304 /* see sylpheed's procmsg.h */
305 if (flags & 2UL) {
306 status.setRead(false);
307 }
308 if ((flags & 3UL) == 0UL) {
309 status.setRead(true);
310 }
311 if (flags & 8UL) {
312 status.setDeleted(true);
313 }
314 if (flags & 16UL) {
315 status.setReplied(true);
316 }
317 if (flags & 32UL) {
318 status.setForwarded(true);
319 }
320 return status;
321 }
322