1 /**
2  * @file
3  * Notmuch database handling
4  *
5  * @authors
6  * Copyright (C) 2018 Richard Russon <rich@flatcap.org>
7  *
8  * @copyright
9  * This program is free software: you can redistribute it and/or modify it under
10  * the terms of the GNU General Public License as published by the Free Software
11  * Foundation, either version 2 of the License, or (at your option) any later
12  * version.
13  *
14  * This program is distributed in the hope that it will be useful, but WITHOUT
15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17  * details.
18  *
19  * You should have received a copy of the GNU General Public License along with
20  * this program.  If not, see <http://www.gnu.org/licenses/>.
21  */
22 
23 /**
24  * @page nm_db Notmuch database handling
25  *
26  * Notmuch database handling
27  */
28 
29 #include "config.h"
30 #include <limits.h>
31 #include <notmuch.h>
32 #include <stdbool.h>
33 #include <stdio.h>
34 #include <string.h>
35 #include <sys/stat.h>
36 #include <time.h>
37 #include "private.h"
38 #include "mutt/lib.h"
39 #include "config/lib.h"
40 #include "email/lib.h"
41 #include "core/lib.h"
42 #include "lib.h"
43 #include "adata.h"
44 #include "mdata.h"
45 #include "mutt_logging.h"
46 
47 /**
48  * nm_db_get_filename - Get the filename of the Notmuch database
49  * @param m Mailbox
50  * @retval ptr Filename
51  *
52  * @note The return value is a pointer into the `$nm_default_url` global variable.
53  *       If that variable changes, the result will be invalid.
54  *       It must not be freed.
55  */
nm_db_get_filename(struct Mailbox * m)56 const char *nm_db_get_filename(struct Mailbox *m)
57 {
58   struct NmMboxData *mdata = nm_mdata_get(m);
59   const char *db_filename = NULL;
60 
61   const char *const c_nm_default_url =
62       cs_subset_string(NeoMutt->sub, "nm_default_url");
63   if (mdata && mdata->db_url && mdata->db_url->path)
64     db_filename = mdata->db_url->path;
65   else
66     db_filename = c_nm_default_url;
67 
68   const char *const c_folder = cs_subset_string(NeoMutt->sub, "folder");
69   if (!db_filename && !c_folder)
70     return NULL;
71 
72   if (!db_filename)
73     db_filename = c_folder;
74 
75   if (nm_path_probe(db_filename, NULL) == MUTT_NOTMUCH)
76     db_filename += NmUrlProtocolLen;
77 
78   mutt_debug(LL_DEBUG2, "nm: db filename '%s'\n", db_filename);
79   return db_filename;
80 }
81 
82 /**
83  * nm_db_do_open - Open a Notmuch database
84  * @param filename Database filename
85  * @param writable Read/write?
86  * @param verbose  Show errors on failure?
87  * @retval ptr Notmuch database
88  */
nm_db_do_open(const char * filename,bool writable,bool verbose)89 notmuch_database_t *nm_db_do_open(const char *filename, bool writable, bool verbose)
90 {
91   notmuch_database_t *db = NULL;
92   int ct = 0;
93   notmuch_status_t st = NOTMUCH_STATUS_SUCCESS;
94 #if LIBNOTMUCH_CHECK_VERSION(4, 2, 0)
95   char *msg = NULL;
96 #endif
97 
98   const short c_nm_open_timeout =
99       cs_subset_number(NeoMutt->sub, "nm_open_timeout");
100   mutt_debug(LL_DEBUG1, "nm: db open '%s' %s (timeout %d)\n", filename,
101              writable ? "[WRITE]" : "[READ]", c_nm_open_timeout);
102 
103   const notmuch_database_mode_t mode =
104       writable ? NOTMUCH_DATABASE_MODE_READ_WRITE : NOTMUCH_DATABASE_MODE_READ_ONLY;
105 
106   do
107   {
108 #if LIBNOTMUCH_CHECK_VERSION(5, 4, 0)
109     // notmuch 0.32-0.32.2 didn't bump libnotmuch version to 5.4.
110     st = notmuch_database_open_with_config(filename, mode, NULL, NULL, &db, &msg);
111     if (st == NOTMUCH_STATUS_NO_CONFIG)
112     {
113       mutt_debug(LL_DEBUG1,
114                  "nm: Could not find a Notmuch configuration file\n");
115       FREE(&msg);
116 
117       st = notmuch_database_open_with_config(filename, mode, "", NULL, &db, &msg);
118     }
119 #elif LIBNOTMUCH_CHECK_VERSION(4, 2, 0)
120     st = notmuch_database_open_verbose(filename, mode, &db, &msg);
121 #elif defined(NOTMUCH_API_3)
122     st = notmuch_database_open(filename, mode, &db);
123 #else
124     db = notmuch_database_open(filename, mode);
125 #endif
126     if ((st == NOTMUCH_STATUS_FILE_ERROR) || db || !c_nm_open_timeout ||
127         ((ct / 2) > c_nm_open_timeout))
128     {
129       break;
130     }
131 
132     if (verbose && ct && ((ct % 2) == 0))
133       mutt_error(_("Waiting for notmuch DB... (%d sec)"), ct / 2);
134     mutt_date_sleep_ms(500); /* Half a second */
135     ct++;
136   } while (true);
137 
138   if (st != NOTMUCH_STATUS_SUCCESS)
139   {
140     db = NULL;
141   }
142 
143   if (verbose)
144   {
145     if (!db)
146     {
147 #if LIBNOTMUCH_CHECK_VERSION(4, 2, 0)
148       if (msg)
149       {
150         mutt_error(msg);
151         FREE(&msg);
152       }
153       else
154 #endif
155       {
156         mutt_error(_("Can't open notmuch database: %s: %s"), filename,
157                    st ? notmuch_status_to_string(st) : _("unknown reason"));
158       }
159     }
160     else if (ct > 1)
161     {
162       mutt_clear_error();
163     }
164   }
165   return db;
166 }
167 
168 /**
169  * nm_db_get - Get the Notmuch database
170  * @param m        Mailbox
171  * @param writable Read/write?
172  * @retval ptr Notmuch database
173  */
nm_db_get(struct Mailbox * m,bool writable)174 notmuch_database_t *nm_db_get(struct Mailbox *m, bool writable)
175 {
176   struct NmAccountData *adata = nm_adata_get(m);
177 
178   if (!adata)
179     return NULL;
180 
181   // Use an existing open db if we have one.
182   if (adata->db)
183     return adata->db;
184 
185   const char *db_filename = nm_db_get_filename(m);
186   if (db_filename)
187     adata->db = nm_db_do_open(db_filename, writable, true);
188 
189   return adata->db;
190 }
191 
192 /**
193  * nm_db_release - Close the Notmuch database
194  * @param m Mailbox
195  * @retval  0 Success
196  * @retval -1 Failure
197  */
nm_db_release(struct Mailbox * m)198 int nm_db_release(struct Mailbox *m)
199 {
200   struct NmAccountData *adata = nm_adata_get(m);
201   if (!adata || !adata->db || nm_db_is_longrun(m))
202     return -1;
203 
204   mutt_debug(LL_DEBUG1, "nm: db close\n");
205   nm_db_free(adata->db);
206   adata->db = NULL;
207   adata->longrun = false;
208   return 0;
209 }
210 
211 /**
212  * nm_db_free - Decoupled way to close a Notmuch database
213  * @param db Notmuch database
214  */
nm_db_free(notmuch_database_t * db)215 void nm_db_free(notmuch_database_t *db)
216 {
217 #ifdef NOTMUCH_API_3
218   notmuch_database_destroy(db);
219 #else
220   notmuch_database_close(db);
221 #endif
222 }
223 
224 /**
225  * nm_db_trans_begin - Start a Notmuch database transaction
226  * @param m Mailbox
227  * @retval <0 error
228  * @retval 1  new transaction started
229  * @retval 0  already within transaction
230  */
nm_db_trans_begin(struct Mailbox * m)231 int nm_db_trans_begin(struct Mailbox *m)
232 {
233   struct NmAccountData *adata = nm_adata_get(m);
234   if (!adata || !adata->db)
235     return -1;
236 
237   if (adata->trans)
238     return 0;
239 
240   mutt_debug(LL_DEBUG2, "nm: db trans start\n");
241   if (notmuch_database_begin_atomic(adata->db))
242     return -1;
243   adata->trans = true;
244   return 1;
245 }
246 
247 /**
248  * nm_db_trans_end - End a database transaction
249  * @param m Mailbox
250  * @retval  0 Success
251  * @retval -1 Failure
252  */
nm_db_trans_end(struct Mailbox * m)253 int nm_db_trans_end(struct Mailbox *m)
254 {
255   struct NmAccountData *adata = nm_adata_get(m);
256   if (!adata || !adata->db)
257     return -1;
258 
259   if (!adata->trans)
260     return 0;
261 
262   mutt_debug(LL_DEBUG2, "nm: db trans end\n");
263   adata->trans = false;
264   if (notmuch_database_end_atomic(adata->db))
265     return -1;
266 
267   return 0;
268 }
269 
270 /**
271  * nm_db_get_mtime - Get the database modification time
272  * @param[in]  m     Mailbox
273  * @param[out] mtime Save the modification time
274  * @retval  0 Success (result in mtime)
275  * @retval -1 Error
276  *
277  * Get the "mtime" (modification time) of the database file.
278  * This is the time of the last update.
279  */
nm_db_get_mtime(struct Mailbox * m,time_t * mtime)280 int nm_db_get_mtime(struct Mailbox *m, time_t *mtime)
281 {
282   if (!m || !mtime)
283     return -1;
284 
285   struct stat st = { 0 };
286   char path[PATH_MAX];
287   const char *db_filename = nm_db_get_filename(m);
288 
289   mutt_debug(LL_DEBUG2, "nm: checking database mtime '%s'\n", db_filename);
290 
291   // See if the path we were given has a Xapian directory.
292   // After notmuch 0.32, a .notmuch folder isn't guaranteed.
293   snprintf(path, sizeof(path), "%s/xapian", db_filename);
294   if (stat(path, &st) == 0)
295   {
296     *mtime = st.st_mtime;
297     return 0;
298   }
299 
300   // Otherwise, check for a .notmuch directory.
301   snprintf(path, sizeof(path), "%s/.notmuch/xapian", db_filename);
302 
303   if (stat(path, &st) != 0)
304     return -1;
305 
306   *mtime = st.st_mtime;
307   return 0;
308 }
309 
310 /**
311  * nm_db_is_longrun - Is Notmuch in the middle of a long-running transaction
312  * @param m Mailbox
313  * @retval true Notmuch is in the middle of a long-running transaction
314  */
nm_db_is_longrun(struct Mailbox * m)315 bool nm_db_is_longrun(struct Mailbox *m)
316 {
317   struct NmAccountData *adata = nm_adata_get(m);
318   if (!adata)
319     return false;
320 
321   return adata->longrun;
322 }
323 
324 /**
325  * nm_db_longrun_init - Start a long transaction
326  * @param m        Mailbox
327  * @param writable Read/write?
328  */
nm_db_longrun_init(struct Mailbox * m,bool writable)329 void nm_db_longrun_init(struct Mailbox *m, bool writable)
330 {
331   struct NmAccountData *adata = nm_adata_get(m);
332 
333   if (!(adata && nm_db_get(m, writable)))
334     return;
335 
336   adata->longrun = true;
337   mutt_debug(LL_DEBUG2, "nm: long run initialized\n");
338 }
339 
340 /**
341  * nm_db_longrun_done - Finish a long transaction
342  * @param m Mailbox
343  */
nm_db_longrun_done(struct Mailbox * m)344 void nm_db_longrun_done(struct Mailbox *m)
345 {
346   struct NmAccountData *adata = nm_adata_get(m);
347 
348   if (adata)
349   {
350     adata->longrun = false; /* to force nm_db_release() released DB */
351     if (nm_db_release(m) == 0)
352       mutt_debug(LL_DEBUG2, "nm: long run deinitialized\n");
353     else
354       adata->longrun = true;
355   }
356 }
357 
358 /**
359  * nm_db_debug_check - Check if the database is open
360  * @param m Mailbox
361  */
nm_db_debug_check(struct Mailbox * m)362 void nm_db_debug_check(struct Mailbox *m)
363 {
364   struct NmAccountData *adata = nm_adata_get(m);
365   if (!adata || !adata->db)
366     return;
367 
368   mutt_debug(LL_DEBUG1, "nm: ERROR: db is open, closing\n");
369   nm_db_release(m);
370 }
371