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