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