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