1 /*
2 ** Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
3 **
4 ** This program is free software; you can redistribute it and/or modify it
5 ** under the terms of the GNU General Public License as published by the
6 ** Free Software Foundation; either version 3, or (at your option) any
7 ** later version.
8 **
9 ** This program is distributed in the hope that it will be useful,
10 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
11 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 ** GNU General Public License for more details.
13 **
14 ** You should have received a copy of the GNU General Public License
15 ** along with this program; if not, write to 59the Free Software Foundation,
16 ** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 **
18 */
19
20 #include "config.h"
21
22 #include <unistd.h>
23 #include <sys/types.h>
24 #include <sys/stat.h>
25 #include <fcntl.h>
26 #include <stdlib.h>
27
28 #include <string.h>
29 #include <errno.h>
30 #include <glib/gprintf.h>
31 #include <gio/gio.h>
32
33 #include "mu-maildir.hh"
34 #include "utils/mu-str.h"
35
36 using namespace Mu;
37
38 #define MU_MAILDIR_NOINDEX_FILE ".noindex"
39 #define MU_MAILDIR_NOUPDATE_FILE ".noupdate"
40
41 /* On Linux (and some BSD), we have entry->d_type, but some file
42 * systems (XFS, ReiserFS) do not support it, and set it DT_UNKNOWN.
43 * On other OSs, notably Solaris, entry->d_type is not present at all.
44 * For these cases, we use lstat (in get_dtype) as a slower fallback,
45 * and return it in the d_type parameter
46 */
47 static unsigned char
get_dtype(struct dirent * dentry,const char * path,gboolean use_lstat)48 get_dtype (struct dirent* dentry, const char *path, gboolean use_lstat)
49 {
50 #ifdef HAVE_STRUCT_DIRENT_D_TYPE
51
52 if (dentry->d_type == DT_UNKNOWN)
53 goto slowpath;
54 if (dentry->d_type == DT_LNK && !use_lstat)
55 goto slowpath;
56
57 return dentry->d_type; /* fastpath */
58
59 slowpath:
60 #endif /*HAVE_STRUCT_DIRENT_D_TYPE*/
61 return mu_util_get_dtype (path, use_lstat);
62 }
63
64 static gboolean
create_maildir(const char * path,mode_t mode,GError ** err)65 create_maildir (const char *path, mode_t mode, GError **err)
66 {
67 int i;
68 const char* subdirs[] = {"new", "cur", "tmp"};
69
70 for (i = 0; i != G_N_ELEMENTS(subdirs); ++i) {
71
72 const char *fullpath;
73 int rv;
74
75 /* static buffer */
76 fullpath = mu_str_fullpath_s (path, subdirs[i]);
77
78 /* if subdir already exists, don't try to re-create
79 * it */
80 if (mu_util_check_dir (fullpath, TRUE, TRUE))
81 continue;
82
83 rv = g_mkdir_with_parents (fullpath, (int)mode);
84
85 /* note, g_mkdir_with_parents won't detect an error if
86 * there's already such a dir, but with the wrong
87 * permissions; so we need to check */
88 if (rv != 0 || !mu_util_check_dir(fullpath, TRUE, TRUE))
89 return mu_util_g_set_error
90 (err,MU_ERROR_FILE_CANNOT_MKDIR,
91 "creating dir failed for %s: %s",
92 fullpath, g_strerror (errno));
93 }
94
95 return TRUE;
96 }
97
98 static gboolean
create_noindex(const char * path,GError ** err)99 create_noindex (const char *path, GError **err)
100 {
101 /* create a noindex file if requested */
102 int fd;
103 const char *noindexpath;
104
105 /* static buffer */
106 noindexpath = mu_str_fullpath_s (path, MU_MAILDIR_NOINDEX_FILE);
107
108 fd = creat (noindexpath, 0644);
109
110 /* note, if the 'close' failed, creation may still have
111 * succeeded...*/
112 if (fd < 0 || close (fd) != 0)
113 return mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_CREATE,
114 "error in create_noindex: %s",
115 g_strerror (errno));
116 return TRUE;
117 }
118
119 gboolean
mu_maildir_mkdir(const char * path,mode_t mode,gboolean noindex,GError ** err)120 Mu::mu_maildir_mkdir (const char* path, mode_t mode, gboolean noindex, GError **err)
121 {
122 g_return_val_if_fail (path, FALSE);
123
124 if (!create_maildir (path, mode, err))
125 return FALSE;
126
127 if (noindex && !create_noindex (path, err))
128 return FALSE;
129
130 return TRUE;
131 }
132
133 /* determine whether the source message is in 'new' or in 'cur';
134 * we ignore messages in 'tmp' for obvious reasons */
135 static gboolean
check_subdir(const char * src,gboolean * in_cur,GError ** err)136 check_subdir (const char *src, gboolean *in_cur, GError **err)
137 {
138 gboolean rv;
139 char *srcpath;
140
141 srcpath = g_path_get_dirname (src);
142 *in_cur = FALSE;
143 rv = TRUE;
144
145 if (g_str_has_suffix (srcpath, "cur"))
146 *in_cur = TRUE;
147 else if (!g_str_has_suffix (srcpath, "new"))
148 rv = mu_util_g_set_error (err,
149 MU_ERROR_FILE_INVALID_SOURCE,
150 "invalid source message '%s'",
151 src);
152 g_free (srcpath);
153 return rv;
154 }
155
156 static char*
get_target_fullpath(const char * src,const char * targetpath,GError ** err)157 get_target_fullpath (const char* src, const char *targetpath, GError **err)
158 {
159 char *targetfullpath, *srcfile;
160 gboolean in_cur;
161
162 if (!check_subdir (src, &in_cur, err))
163 return NULL;
164
165 srcfile = g_path_get_basename (src);
166
167 /* create targetpath; note: make the filename *cough* unique
168 * by including a hash of the srcname in the targetname. This
169 * helps if there are copies of a message (which all have the
170 * same basename)
171 */
172 targetfullpath = g_strdup_printf ("%s%c%s%c%u_%s",
173 targetpath,
174 G_DIR_SEPARATOR,
175 in_cur ? "cur" : "new",
176 G_DIR_SEPARATOR,
177 g_str_hash(src),
178 srcfile);
179 g_free (srcfile);
180
181 return targetfullpath;
182 }
183
184
185 gboolean
mu_maildir_link(const char * src,const char * targetpath,GError ** err)186 Mu::mu_maildir_link (const char* src, const char *targetpath, GError **err)
187 {
188 char *targetfullpath;
189 int rv;
190
191 g_return_val_if_fail (src, FALSE);
192 g_return_val_if_fail (targetpath, FALSE);
193
194 targetfullpath = get_target_fullpath (src, targetpath, err);
195 if (!targetfullpath)
196 return FALSE;
197
198 rv = symlink (src, targetfullpath);
199
200 if (rv != 0)
201 mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_LINK,
202 "error creating link %s => %s: %s",
203 targetfullpath, src, g_strerror (errno));
204 g_free (targetfullpath);
205
206 return rv == 0 ? TRUE: FALSE;
207 }
208
209
210
211 /*
212 * determine if path is a maildir leaf-dir; ie. if it's 'cur' or 'new'
213 * (we're skipping 'tmp' for obvious reasons)
214 */
215 gboolean
mu_maildir_is_leaf_dir(const char * path)216 Mu::mu_maildir_is_leaf_dir (const char *path)
217 {
218 size_t len;
219
220 /* path is the full path; it cannot possibly be shorter
221 * than 4 for a maildir (/cur or /new) */
222 len = path ? strlen (path) : 0;
223 if (G_UNLIKELY(len < 4))
224 return FALSE;
225
226 /* optimization; one further idea would be cast the 4 bytes to an
227 * integer and compare that -- need to think about alignment,
228 * endianness */
229
230 if (path[len - 4] == G_DIR_SEPARATOR &&
231 path[len - 3] == 'c' &&
232 path[len - 2] == 'u' &&
233 path[len - 1] == 'r')
234 return TRUE;
235
236 if (path[len - 4] == G_DIR_SEPARATOR &&
237 path[len - 3] == 'n' &&
238 path[len - 2] == 'e' &&
239 path[len - 1] == 'w')
240 return TRUE;
241
242 return FALSE;
243 }
244
245
246 static gboolean
clear_links(const char * path,DIR * dir)247 clear_links (const char *path, DIR *dir)
248 {
249 gboolean rv;
250 struct dirent *dentry;
251
252 rv = TRUE;
253 errno = 0;
254
255 while ((dentry = readdir (dir))) {
256
257 guint8 d_type;
258 char *fullpath;
259
260 if (dentry->d_name[0] == '.')
261 continue; /* ignore .,.. other dotdirs */
262
263 fullpath = g_build_path ("/", path, dentry->d_name, NULL);
264 d_type = get_dtype (dentry, fullpath, TRUE/*lstat*/);
265
266 if (d_type == DT_LNK) {
267 if (unlink (fullpath) != 0 ) {
268 g_warning ("error unlinking %s: %s",
269 fullpath, g_strerror(errno));
270 rv = FALSE;
271 }
272 } else if (d_type == DT_DIR) {
273 DIR *subdir;
274 subdir = opendir (fullpath);
275 if (!subdir) {
276 g_warning ("failed to open dir %s: %s",
277 fullpath, g_strerror(errno));
278 rv = FALSE;
279 goto next;
280 }
281
282 if (!clear_links (fullpath, subdir))
283 rv = FALSE;
284
285 closedir (subdir);
286 }
287
288 next:
289 g_free (fullpath);
290 }
291
292 return rv;
293 }
294
295 gboolean
mu_maildir_clear_links(const char * path,GError ** err)296 Mu::mu_maildir_clear_links (const char *path, GError **err)
297 {
298 DIR *dir;
299 gboolean rv;
300
301 g_return_val_if_fail (path, FALSE);
302
303 dir = opendir (path);
304 if (!dir) {
305 g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE_CANNOT_OPEN,
306 "failed to open %s: %s", path, g_strerror(errno));
307 return FALSE;
308 }
309
310 rv = clear_links (path, dir);
311
312 closedir (dir);
313
314 return rv;
315 }
316
317 MuFlags
mu_maildir_get_flags_from_path(const char * path)318 Mu::mu_maildir_get_flags_from_path (const char *path)
319 {
320 g_return_val_if_fail (path, MU_FLAG_INVALID);
321
322 /* try to find the info part */
323 /* note that we can use either the ':', ';', or '!' as separator;
324 * the former is the official, but as it does not work on e.g. VFAT
325 * file systems, some Maildir implementations use the latter instead
326 * (or both). For example, Tinymail/modest does this. The python
327 * documentation at http://docs.python.org/lib/mailbox-maildir.html
328 * mentions the '!' as well as a 'popular choice'. Isync uses ';' by
329 * default on Windows.
330 */
331
332 /* we check the dir -- */
333 if (strstr (path, G_DIR_SEPARATOR_S "new" G_DIR_SEPARATOR_S)) {
334
335 char *dir, *dir2;
336 MuFlags flags;
337
338 dir = g_path_get_dirname (path);
339 dir2 = g_path_get_basename (dir);
340
341 flags = MU_FLAG_NONE;
342
343 if (g_strcmp0 (dir2, "new") == 0)
344 flags = MU_FLAG_NEW;
345
346 g_free (dir);
347 g_free (dir2);
348
349 /* NOTE: new/ message should not have :2,-stuff, as
350 * per http://cr.yp.to/proto/maildir.html. If they, do
351 * we ignore it
352 */
353 if (flags == MU_FLAG_NEW)
354 return flags;
355 }
356
357 /* get the file flags */
358 {
359 const char *info;
360
361 info = strrchr (path, '2');
362 if (!info || info == path ||
363 (info[-1] != ':' && info[-1] != '!' && info[-1] != ';') ||
364 (info[1] != ','))
365 return MU_FLAG_NONE;
366 else
367 return mu_flags_from_str
368 (&info[2], MU_FLAG_TYPE_MAILFILE,
369 TRUE /*ignore invalid */);
370 }
371 }
372
373
374 /*
375 * take an existing message path, and return a new path, based on
376 * whether it should be in 'new' or 'cur'; ie.
377 *
378 * /home/user/Maildir/foo/bar/cur/abc:2,F and flags == MU_FLAG_NEW
379 * => /home/user/Maildir/foo/bar/new
380 * and
381 * /home/user/Maildir/foo/bar/new/abc and flags == MU_FLAG_REPLIED
382 * => /home/user/Maildir/foo/bar/cur
383 *
384 * so the difference is whether MU_FLAG_NEW is set or not; and in the
385 * latter case, no other flags are allowed.
386 *
387 */
388 static char*
get_new_path(const char * mdir,const char * mfile,MuFlags flags,const char * custom_flags,char flags_sep)389 get_new_path (const char *mdir, const char *mfile, MuFlags flags,
390 const char* custom_flags, char flags_sep)
391 {
392 if (flags & MU_FLAG_NEW)
393 return g_strdup_printf ("%s%cnew%c%s",
394 mdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR,
395 mfile);
396 else {
397 const char *flagstr;
398 flagstr = mu_flags_to_str_s (flags, MU_FLAG_TYPE_MAILFILE);
399
400 return g_strdup_printf ("%s%ccur%c%s%c2,%s%s",
401 mdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR,
402 mfile, flags_sep, flagstr,
403 custom_flags ? custom_flags : "");
404 }
405 }
406
407
408 char*
mu_maildir_get_maildir_from_path(const char * path)409 Mu::mu_maildir_get_maildir_from_path (const char* path)
410 {
411 char *mdir;
412
413 /* determine the maildir */
414 mdir = g_path_get_dirname (path);
415 if (!g_str_has_suffix (mdir, "cur") &&
416
417 !g_str_has_suffix (mdir, "new")) {
418 g_warning ("%s: not a valid maildir path: %s",
419 __func__, path);
420 g_free (mdir);
421 return NULL;
422 }
423
424 /* remove the 'cur' or 'new' */
425 mdir[strlen(mdir) - 4] = '\0';
426
427 return mdir;
428 }
429
430
431 static char*
get_new_basename(void)432 get_new_basename (void)
433 {
434 return g_strdup_printf ("%u.%08x%08x.%s",
435 (guint)time(NULL),
436 g_random_int(),
437 (gint32)g_get_monotonic_time (),
438 g_get_host_name ());
439 }
440
441 static char*
find_path_separator(const char * path)442 find_path_separator(const char *path)
443 {
444 const char *cur;
445 for (cur = &path[strlen(path)-1]; cur > path; --cur) {
446 if ((*cur == ':' || *cur == '!' || *cur == ';') &&
447 (cur[1] == '2' && cur[2] == ',')) {
448 return (char*)cur;
449 }
450 }
451 return NULL;
452 }
453
454 char*
mu_maildir_get_new_path(const char * oldpath,const char * new_mdir,MuFlags newflags,gboolean new_name)455 Mu::mu_maildir_get_new_path (const char *oldpath, const char *new_mdir,
456 MuFlags newflags, gboolean new_name)
457 {
458 char *mfile, *mdir, *custom_flags, *cur, *newpath, flags_sep = ':';
459
460 g_return_val_if_fail (oldpath, NULL);
461
462 mfile = newpath = custom_flags = NULL;
463
464 /* determine the maildir */
465 mdir = mu_maildir_get_maildir_from_path (oldpath);
466 if (!mdir)
467 return NULL;
468
469 /* determine the name of the location of the flag separator */
470
471 if (new_name) {
472 mfile = get_new_basename ();
473 cur = find_path_separator (oldpath);
474 if (cur) {
475 /* preserve the existing flags separator
476 * in the new file name */
477 flags_sep = *cur;
478 }
479 } else {
480 mfile = g_path_get_basename (oldpath);
481 cur = find_path_separator (mfile);
482 if (cur) {
483 /* get the custom flags (if any) */
484 custom_flags = mu_flags_custom_from_str (cur + 3);
485 /* preserve the existing flags separator
486 * in the new file name */
487 flags_sep = *cur;
488 cur[0] = '\0'; /* strip the flags */
489 }
490 }
491
492 newpath = get_new_path (new_mdir ? new_mdir : mdir,
493 mfile, newflags, custom_flags, flags_sep);
494 g_free (mfile);
495 g_free (mdir);
496 g_free (custom_flags);
497
498 return newpath;
499 }
500
501
502 static gint64
get_file_size(const char * path)503 get_file_size (const char* path)
504 {
505 int rv;
506 struct stat statbuf;
507
508 rv = stat (path, &statbuf);
509 if (rv != 0) {
510 /* g_warning ("error: %s", g_strerror (errno)); */
511 return -1;
512 }
513
514 return (gint64)statbuf.st_size;
515 }
516
517
518 static gboolean
msg_move_check_pre(const char * src,const char * dst,GError ** err)519 msg_move_check_pre (const char *src, const char *dst, GError **err)
520 {
521 gint size1, size2;
522
523 if (!g_path_is_absolute(src))
524 return mu_util_g_set_error
525 (err, MU_ERROR_FILE,
526 "source is not an absolute path: '%s'", src);
527
528 if (!g_path_is_absolute(dst))
529 return mu_util_g_set_error
530 (err, MU_ERROR_FILE,
531 "target is not an absolute path: '%s'", dst);
532
533 if (access (src, R_OK) != 0)
534 return mu_util_g_set_error (err, MU_ERROR_FILE,
535 "cannot read %s", src);
536
537 if (access (dst, F_OK) != 0)
538 return TRUE;
539
540 /* target exist; we simply overwrite it, unless target has a different
541 * size. ignore the exceedingly rare case where have duplicate message
542 * file names with different content yet the same length. (md5 etc. is a
543 * bit slow) */
544 size1 = get_file_size (src);
545 size2 = get_file_size (dst);
546 if (size1 != size2)
547 return mu_util_g_set_error (err, MU_ERROR_FILE,
548 "%s already exists", dst);
549
550 return TRUE;
551 }
552
553 static gboolean
msg_move_check_post(const char * src,const char * dst,GError ** err)554 msg_move_check_post (const char *src, const char *dst, GError **err)
555 {
556 /* double check -- is the target really there? */
557 if (access (dst, F_OK) != 0)
558 return mu_util_g_set_error
559 (err, MU_ERROR_FILE, "can't find target (%s->%s)", src, dst);
560
561 if (access (src, F_OK) == 0) {
562
563 if (g_strcmp0(src, dst) == 0) {
564 g_warning ("moved %s to itself", src);
565 return TRUE;
566 }
567
568 /* this could happen if some other tool (for mail syncing) is
569 * interfering */
570 g_debug ("the source is still there (%s->%s)",
571 src, dst);
572 }
573
574 return TRUE;
575 }
576
577 /* use GIO to move files; this is slower than rename() so only use
578 * this when needed: when moving across filesystems */
579 static gboolean
msg_move_g_file(const char * src,const char * dst,GError ** err)580 msg_move_g_file (const char* src, const char *dst, GError **err)
581 {
582 GFile *srcfile, *dstfile;
583 gboolean res;
584
585 srcfile = g_file_new_for_path (src);
586 dstfile = g_file_new_for_path (dst);
587
588 res = g_file_move (srcfile, dstfile, G_FILE_COPY_NONE,
589 NULL, NULL, NULL, err);
590
591 g_clear_object (&srcfile);
592 g_clear_object (&dstfile);
593
594 return res;
595 }
596
597 static gboolean
msg_move(const char * src,const char * dst,GError ** err)598 msg_move (const char* src, const char *dst, GError **err)
599 {
600 if (!msg_move_check_pre (src, dst, err))
601 return FALSE;
602
603 if (rename (src, dst) == 0) /* seems it worked. */
604 return msg_move_check_post (src, dst, err);
605
606 if (errno != EXDEV) /* some unrecoverable error occurred */
607 return mu_util_g_set_error
608 (err, MU_ERROR_FILE, "error moving %s -> %s", src, dst);
609
610 /* he EXDEV case -- source and target live on different filesystems */
611 return msg_move_g_file (src, dst, err);
612 }
613
614 char*
mu_maildir_move_message(const char * oldpath,const char * targetmdir,MuFlags newflags,gboolean ignore_dups,gboolean new_name,GError ** err)615 Mu::mu_maildir_move_message (const char* oldpath, const char* targetmdir,
616 MuFlags newflags, gboolean ignore_dups,
617 gboolean new_name, GError **err)
618 {
619 char *newfullpath;
620 gboolean rv;
621 gboolean src_is_target;
622
623 g_return_val_if_fail (oldpath, FALSE);
624
625 /* first try *without* changing the name (as per new_name), since
626 * src_is_target shouldn't use a changed name */
627 newfullpath = mu_maildir_get_new_path (oldpath, targetmdir,
628 newflags, FALSE);
629 if (!newfullpath) {
630 mu_util_g_set_error (err, MU_ERROR_FILE,
631 "failed to determine targetpath");
632 return NULL;
633 }
634
635 src_is_target = (g_strcmp0 (oldpath, newfullpath) == 0);
636 if (!ignore_dups && src_is_target) {
637 mu_util_g_set_error (err, MU_ERROR_FILE_TARGET_EQUALS_SOURCE,
638 "target equals source");
639 return NULL;
640 }
641
642 /* if we generated file is not the same (modulo flags), create a fully
643 * new name in the new_name case */
644 if (!src_is_target && new_name) {
645 g_free(newfullpath);
646 newfullpath = mu_maildir_get_new_path (oldpath, targetmdir,
647 newflags, new_name);
648 if (!newfullpath) {
649 mu_util_g_set_error (err, MU_ERROR_FILE,
650 "failed to determine targetpath");
651 return NULL;
652 }
653 }
654
655 if (!src_is_target) {
656 g_debug ("moving %s (%s, %x, %d) --> %s",
657 oldpath, targetmdir, newflags, new_name,
658 newfullpath);
659 rv = msg_move (oldpath, newfullpath, err);
660 if (!rv) {
661 g_free (newfullpath);
662 return NULL;
663 }
664 }
665
666 return newfullpath;
667 }
668