0
|
1 /*
|
|
2 database.c
|
|
3
|
|
4 $Id: database.c 3 2000-01-04 11:35:42Z enz $
|
|
5
|
|
6 Uses GNU gdbm library. Using Berkeley db (included in libc6) was
|
|
7 cumbersome. It is based on Berkeley db 1.85, which has severe bugs
|
|
8 (e.g. it is not recommended to delete or overwrite entries with
|
|
9 overflow pages).
|
|
10 */
|
|
11
|
|
12 #include "database.h"
|
|
13 #include <ctype.h>
|
|
14 #include <errno.h>
|
|
15 #include <fcntl.h>
|
|
16 #include <gdbm.h>
|
|
17 #include <unistd.h>
|
|
18 #include <sys/types.h>
|
|
19 #include <sys/stat.h>
|
|
20 #include "config.h"
|
|
21 #include "log.h"
|
|
22 #include "protocol.h"
|
|
23 #include "util.h"
|
|
24
|
|
25 static struct Db
|
|
26 {
|
|
27 GDBM_FILE dbf;
|
|
28
|
|
29 /* Start string for Xref header line: "Xref: <host>" */
|
|
30 Str xrefHost;
|
|
31
|
|
32 /* Msg Id of presently loaded article, empty if none loaded */
|
|
33 Str msgId;
|
|
34
|
|
35 /* Status of loaded article */
|
|
36 int stat; /* Flags */
|
|
37 time_t lastAccess;
|
|
38
|
|
39 /* Overview of loaded article */
|
|
40 Str subj;
|
|
41 Str from;
|
|
42 Str date;
|
|
43 Str ref;
|
|
44 Str xref;
|
|
45 size_t bytes;
|
|
46 size_t lines;
|
|
47
|
|
48 /* Article text (except for overview header lines) */
|
|
49 DynStr *txt;
|
|
50
|
|
51 } db = { NULL, "(unknown)", "", 0, 0, "", "", "", "", "", 0, 0, NULL };
|
|
52
|
|
53 static const char *
|
|
54 errMsg( void )
|
|
55 {
|
|
56 if ( errno != 0 )
|
|
57 return strerror( errno );
|
|
58 return gdbm_strerror( gdbm_errno );
|
|
59 }
|
|
60
|
|
61 Bool
|
|
62 Db_open( void )
|
|
63 {
|
|
64 Str name, host;
|
|
65 int flags;
|
|
66
|
|
67 ASSERT( db.dbf == NULL );
|
|
68 snprintf( name, MAXCHAR, "%s/data/articles.gdbm", Cfg_spoolDir() );
|
|
69 flags = GDBM_WRCREAT | GDBM_FAST;
|
|
70
|
|
71 if ( ! ( db.dbf = gdbm_open( name, 512, flags, 0644, NULL ) ) )
|
|
72 {
|
|
73 Log_err( "Error opening %s for r/w (%s)", name, errMsg() );
|
|
74 return FALSE;
|
|
75 }
|
|
76 Log_dbg( "%s opened for r/w", name );
|
|
77
|
|
78 if ( db.txt == NULL )
|
|
79 db.txt = new_DynStr( 5000 );
|
|
80
|
|
81 gethostname( host, MAXCHAR );
|
|
82 snprintf( db.xrefHost, MAXCHAR, "Xref: %s", host );
|
|
83
|
|
84 return TRUE;
|
|
85 }
|
|
86
|
|
87 void
|
|
88 Db_close( void )
|
|
89 {
|
|
90 ASSERT( db.dbf );
|
|
91 Log_dbg( "Closing database" );
|
|
92 gdbm_close( db.dbf );
|
|
93 db.dbf = NULL;
|
|
94 del_DynStr( db.txt );
|
|
95 db.txt = NULL;
|
|
96 Utl_cpyStr( db.msgId, "" );
|
|
97 }
|
|
98
|
|
99 static Bool
|
|
100 loadArt( const char *msgId )
|
|
101 {
|
|
102 static void *dptr = NULL;
|
|
103
|
|
104 datum key, val;
|
|
105 Str t = "";
|
|
106 const char *p;
|
|
107
|
|
108 ASSERT( db.dbf );
|
|
109
|
|
110 if ( strcmp( msgId, db.msgId ) == 0 )
|
|
111 return TRUE;
|
|
112
|
|
113 key.dptr = (void *)msgId;
|
|
114 key.dsize = strlen( msgId ) + 1;
|
|
115 if ( dptr != NULL )
|
|
116 {
|
|
117 free( dptr );
|
|
118 dptr = NULL;
|
|
119 }
|
|
120 val = gdbm_fetch( db.dbf, key );
|
|
121 dptr = val.dptr;
|
|
122 if ( dptr == NULL )
|
|
123 {
|
|
124 Log_dbg( "database.c loadArt: gdbm_fetch found no entry" );
|
|
125 return FALSE;
|
|
126 }
|
|
127
|
|
128 Utl_cpyStr( db.msgId, msgId );
|
|
129 p = Utl_getLn( t, (char *)dptr );
|
|
130 if ( ! p || sscanf( t, "%x", &db.stat ) != 1 )
|
|
131 {
|
|
132 Log_err( "Entry in database '%s' is corrupt (status)", msgId );
|
|
133 return FALSE;
|
|
134 }
|
|
135 p = Utl_getLn( t, p );
|
|
136 if ( ! p || sscanf( t, "%lu", &db.lastAccess ) != 1 )
|
|
137 {
|
|
138 Log_err( "Entry in database '%s' is corrupt (lastAccess)", msgId );
|
|
139 return FALSE;
|
|
140 }
|
|
141 p = Utl_getLn( db.subj, p );
|
|
142 p = Utl_getLn( db.from, p );
|
|
143 p = Utl_getLn( db.date, p );
|
|
144 p = Utl_getLn( db.ref, p );
|
|
145 p = Utl_getLn( db.xref, p );
|
|
146 if ( ! p )
|
|
147 {
|
|
148 Log_err( "Entry in database '%s' is corrupt (overview)", msgId );
|
|
149 return FALSE;
|
|
150 }
|
|
151 p = Utl_getLn( t, p );
|
|
152 if ( ! p || sscanf( t, "%u", &db.bytes ) != 1 )
|
|
153 {
|
|
154 Log_err( "Entry in database '%s' is corrupt (bytes)", msgId );
|
|
155 return FALSE;
|
|
156 }
|
|
157 p = Utl_getLn( t, p );
|
|
158 if ( ! p || sscanf( t, "%u", &db.lines ) != 1 )
|
|
159 {
|
|
160 Log_err( "Entry in database '%s' is corrupt (lines)", msgId );
|
|
161 return FALSE;
|
|
162 }
|
|
163 DynStr_clear( db.txt );
|
|
164 DynStr_app( db.txt, p );
|
|
165 return TRUE;
|
|
166 }
|
|
167
|
|
168 static Bool
|
|
169 saveArt( void )
|
|
170 {
|
|
171 DynStr *s;
|
|
172 Str t = "";
|
|
173 datum key, val;
|
|
174
|
|
175 if ( strcmp( db.msgId, "" ) == 0 )
|
|
176 return FALSE;
|
|
177 s = new_DynStr( 5000 );
|
|
178 snprintf( t, MAXCHAR, "%x", db.stat );
|
|
179 DynStr_appLn( s, t );
|
|
180 snprintf( t, MAXCHAR, "%lu", db.lastAccess );
|
|
181 DynStr_appLn( s, t );
|
|
182 DynStr_appLn( s, db.subj );
|
|
183 DynStr_appLn( s, db.from );
|
|
184 DynStr_appLn( s, db.date );
|
|
185 DynStr_appLn( s, db.ref );
|
|
186 DynStr_appLn( s, db.xref );
|
|
187 snprintf( t, MAXCHAR, "%u", db.bytes );
|
|
188 DynStr_appLn( s, t );
|
|
189 snprintf( t, MAXCHAR, "%u", db.lines );
|
|
190 DynStr_appLn( s, t );
|
|
191 DynStr_appDynStr( s, db.txt );
|
|
192
|
|
193 key.dptr = (void *)db.msgId;
|
|
194 key.dsize = strlen( db.msgId ) + 1;
|
|
195 val.dptr = (void *)DynStr_str( s );
|
|
196 val.dsize = DynStr_len( s ) + 1;
|
|
197 if ( gdbm_store( db.dbf, key, val, GDBM_REPLACE ) != 0 )
|
|
198 {
|
|
199 Log_err( "Could not store %s in database (%s)", errMsg() );
|
|
200 return FALSE;
|
|
201 }
|
|
202
|
|
203 del_DynStr( s );
|
|
204 return TRUE;
|
|
205 }
|
|
206
|
|
207 Bool
|
|
208 Db_prepareEntry( const Over *ov, const char *grp, int numb )
|
|
209 {
|
|
210 const char *msgId;
|
|
211
|
|
212 ASSERT( db.dbf );
|
|
213 ASSERT( ov );
|
|
214 ASSERT( grp );
|
|
215
|
|
216 msgId = Ov_msgId( ov );
|
|
217 Log_dbg( "Preparing entry %s", msgId );
|
|
218 if ( Db_contains( msgId ) )
|
|
219 Log_err( "Preparing article twice: %s", msgId );
|
|
220
|
|
221 db.stat = DB_NOT_DOWNLOADED;
|
|
222 db.lastAccess = time( NULL );
|
|
223
|
|
224 Utl_cpyStr( db.msgId, msgId );
|
|
225 Utl_cpyStr( db.subj, Ov_subj( ov ) );
|
|
226 Utl_cpyStr( db.from, Ov_from( ov ) );
|
|
227 Utl_cpyStr( db.date, Ov_date( ov ) );
|
|
228 Utl_cpyStr( db.ref, Ov_ref( ov ) );
|
|
229 snprintf( db.xref, MAXCHAR, "%s:%i", grp, numb );
|
|
230 db.bytes = Ov_bytes( ov );
|
|
231 db.lines = Ov_lines( ov );
|
|
232
|
|
233 DynStr_clear( db.txt );
|
|
234
|
|
235 return saveArt();
|
|
236 }
|
|
237
|
|
238 Bool
|
|
239 Db_storeArt( const char *msgId, const char *artTxt )
|
|
240 {
|
|
241 Str line, lineEx, field, value;
|
|
242 const char *startPos;
|
|
243
|
|
244 ASSERT( db.dbf );
|
|
245
|
|
246 Log_dbg( "Store article %s", msgId );
|
|
247 if ( ! loadArt( msgId ) )
|
|
248 {
|
|
249 Log_err( "Cannot find info about '%s' in database", msgId );
|
|
250 return FALSE;
|
|
251 }
|
|
252 if ( ! ( db.stat & DB_NOT_DOWNLOADED ) )
|
|
253 {
|
|
254 Log_err( "Trying to store alrady retrieved article '%s'", msgId );
|
|
255 return FALSE;
|
|
256 }
|
|
257 db.stat &= ~DB_NOT_DOWNLOADED;
|
|
258 db.stat &= ~DB_RETRIEVING_FAILED;
|
|
259 db.lastAccess = time( NULL );
|
|
260
|
|
261 DynStr_clear( db.txt );
|
|
262
|
|
263 /* Read header */
|
|
264 startPos = artTxt;
|
|
265 while ( TRUE )
|
|
266 {
|
|
267 artTxt = Utl_getLn( lineEx, artTxt );
|
|
268 if ( lineEx[ 0 ] == '\0' )
|
|
269 {
|
|
270 DynStr_appLn( db.txt, lineEx );
|
|
271 break;
|
|
272 }
|
|
273 /* Get other lines if field is split over multiple lines */
|
|
274 while ( ( artTxt = Utl_getLn( line, artTxt ) ) )
|
|
275 if ( isspace( line[ 0 ] ) )
|
|
276 {
|
|
277 strncat( lineEx, "\n", MAXCHAR );
|
|
278 strncat( lineEx, line, MAXCHAR );
|
|
279 }
|
|
280 else
|
|
281 {
|
|
282 artTxt = Utl_ungetLn( startPos, artTxt );
|
|
283 break;
|
|
284 }
|
|
285 /* Remove fields already in overview and handle x-noffle
|
|
286 headers correctly in case of cascading NOFFLEs */
|
|
287 if ( Prt_getField( field, value, lineEx ) )
|
|
288 {
|
|
289 if ( strcmp( field, "x-noffle-status" ) == 0 )
|
|
290 {
|
|
291 if ( strstr( value, "NOT_DOWNLOADED" ) != 0 )
|
|
292 db.stat |= DB_NOT_DOWNLOADED;
|
|
293 }
|
|
294 else if ( strcmp( field, "message-id" ) != 0
|
|
295 && strcmp( field, "xref" ) != 0
|
|
296 && strcmp( field, "references" ) != 0
|
|
297 && strcmp( field, "subject" ) != 0
|
|
298 && strcmp( field, "from" ) != 0
|
|
299 && strcmp( field, "date" ) != 0
|
|
300 && strcmp( field, "bytes" ) != 0
|
|
301 && strcmp( field, "lines" ) != 0
|
|
302 && strcmp( field, "x-noffle-lastaccess" ) != 0 )
|
|
303 DynStr_appLn( db.txt, lineEx );
|
|
304 }
|
|
305 }
|
|
306
|
|
307 /* Read body */
|
|
308 while ( ( artTxt = Utl_getLn( line, artTxt ) ) )
|
|
309 if ( ! ( db.stat & DB_NOT_DOWNLOADED ) )
|
|
310 DynStr_appLn( db.txt, line );
|
|
311
|
|
312 return saveArt();
|
|
313 }
|
|
314
|
|
315 void
|
|
316 Db_setStat( const char *msgId, int stat )
|
|
317 {
|
|
318 if ( loadArt( msgId ) )
|
|
319 {
|
|
320 db.stat = stat;
|
|
321 saveArt();
|
|
322 }
|
|
323 }
|
|
324
|
|
325 void
|
|
326 Db_updateLastAccess( const char *msgId )
|
|
327 {
|
|
328 if ( loadArt( msgId ) )
|
|
329 {
|
|
330 db.lastAccess = time( NULL );
|
|
331 saveArt();
|
|
332 }
|
|
333 }
|
|
334
|
|
335 void
|
|
336 Db_setXref( const char *msgId, const char *xref )
|
|
337 {
|
|
338 if ( loadArt( msgId ) )
|
|
339 {
|
|
340 Utl_cpyStr( db.xref, xref );
|
|
341 saveArt();
|
|
342 }
|
|
343 }
|
|
344
|
|
345 /* Search best position for breaking a line */
|
|
346 static const char *
|
|
347 searchBreakPos( const char *line, int wantedLength )
|
|
348 {
|
|
349 const char *lastSpace = NULL;
|
|
350 Bool lastWasSpace = FALSE;
|
|
351 int len = 0;
|
|
352
|
|
353 while ( *line != '\0' )
|
|
354 {
|
|
355 if ( isspace( *line ) )
|
|
356 {
|
|
357 if ( len > wantedLength && lastSpace != NULL )
|
|
358 return lastSpace;
|
|
359 if ( ! lastWasSpace )
|
|
360 lastSpace = line;
|
|
361 lastWasSpace = TRUE;
|
|
362 }
|
|
363 else
|
|
364 lastWasSpace = FALSE;
|
|
365 ++len;
|
|
366 ++line;
|
|
367 }
|
|
368 if ( len > wantedLength && lastSpace != NULL )
|
|
369 return lastSpace;
|
|
370 return line;
|
|
371 }
|
|
372
|
|
373 /* Append header line by breaking long line into multiple lines */
|
|
374 static void
|
|
375 appendLongHeader( DynStr *target, const char *field, const char *value )
|
|
376 {
|
|
377 const int wantedLength = 78;
|
|
378 const char *breakPos, *old;
|
|
379 int len;
|
|
380
|
|
381 len = strlen( field );
|
|
382 DynStr_appN( target, field, len );
|
|
383 DynStr_appN( target, " ", 1 );
|
|
384 old = value;
|
|
385 while ( isspace( *old ) )
|
|
386 ++old;
|
|
387 breakPos = searchBreakPos( old, wantedLength - len - 1 );
|
|
388 DynStr_appN( target, old, breakPos - old );
|
|
389 if ( *breakPos == '\0' )
|
|
390 {
|
|
391 DynStr_appN( target, "\n", 1 );
|
|
392 return;
|
|
393 }
|
|
394 DynStr_appN( target, "\n ", 2 );
|
|
395 while ( TRUE )
|
|
396 {
|
|
397 old = breakPos;
|
|
398 while ( isspace( *old ) )
|
|
399 ++old;
|
|
400 breakPos = searchBreakPos( old, wantedLength - 1 );
|
|
401 DynStr_appN( target, old, breakPos - old );
|
|
402 if ( *breakPos == '\0' )
|
|
403 {
|
|
404 DynStr_appN( target, "\n", 1 );
|
|
405 return;
|
|
406 }
|
|
407 DynStr_appN( target, "\n ", 2 );
|
|
408 }
|
|
409 }
|
|
410
|
|
411 const char *
|
|
412 Db_header( const char *msgId )
|
|
413 {
|
|
414 static DynStr *s = NULL;
|
|
415
|
|
416 Str date, t;
|
|
417 int stat;
|
|
418 const char *p;
|
|
419
|
|
420 if ( s == NULL )
|
|
421 s = new_DynStr( 5000 );
|
|
422 else
|
|
423 DynStr_clear( s );
|
|
424 ASSERT( db.dbf );
|
|
425 if ( ! loadArt( msgId ) )
|
|
426 return NULL;
|
|
427 strftime( date, MAXCHAR, "%Y-%m-%d %H:%M:%S",
|
|
428 localtime( &db.lastAccess ) );
|
|
429 stat = db.stat;
|
|
430 snprintf( t, MAXCHAR,
|
|
431 "Message-ID: %s\n"
|
|
432 "X-NOFFLE-Status:%s%s%s\n"
|
|
433 "X-NOFFLE-LastAccess: %s\n",
|
|
434 msgId,
|
|
435 stat & DB_INTERESTING ? " INTERESTING" : "",
|
|
436 stat & DB_NOT_DOWNLOADED ? " NOT_DOWNLOADED" : "",
|
|
437 stat & DB_RETRIEVING_FAILED ? " RETRIEVING_FAILED" : "",
|
|
438 date );
|
|
439 DynStr_app( s, t );
|
|
440 appendLongHeader( s, "Subject:", db.subj );
|
|
441 appendLongHeader( s, "From:", db.from );
|
|
442 appendLongHeader( s, "Date:", db.date );
|
|
443 appendLongHeader( s, "References:", db.ref );
|
|
444 DynStr_app( s, "Bytes: " );
|
|
445 snprintf( t, MAXCHAR, "%u", db.bytes );
|
|
446 DynStr_appLn( s, t );
|
|
447 DynStr_app( s, "Lines: " );
|
|
448 snprintf( t, MAXCHAR, "%u", db.lines );
|
|
449 DynStr_appLn( s, t );
|
|
450 appendLongHeader( s, db.xrefHost, db.xref );
|
|
451 p = strstr( DynStr_str( db.txt ), "\n\n" );
|
|
452 if ( ! p )
|
|
453 DynStr_appDynStr( s, db.txt );
|
|
454 else
|
|
455 DynStr_appN( s, DynStr_str( db.txt ), p - DynStr_str( db.txt ) + 1 );
|
|
456 return DynStr_str( s );
|
|
457 }
|
|
458
|
|
459 const char *
|
|
460 Db_body( const char *msgId )
|
|
461 {
|
|
462 const char *p;
|
|
463
|
|
464 if ( ! loadArt( msgId ) )
|
|
465 return "";
|
|
466 p = strstr( DynStr_str( db.txt ), "\n\n" );
|
|
467 if ( ! p )
|
|
468 return "";
|
|
469 return ( p + 2 );
|
|
470 }
|
|
471
|
|
472 int
|
|
473 Db_stat( const char *msgId )
|
|
474 {
|
|
475 if ( ! loadArt( msgId ) )
|
|
476 return 0;
|
|
477 return db.stat;
|
|
478 }
|
|
479
|
|
480 time_t
|
|
481 Db_lastAccess( const char *msgId )
|
|
482 {
|
|
483 if ( ! loadArt( msgId ) )
|
|
484 return -1;
|
|
485 return db.lastAccess;
|
|
486 }
|
|
487
|
|
488 const char *
|
|
489 Db_ref( const char *msgId )
|
|
490 {
|
|
491 if ( ! loadArt( msgId ) )
|
|
492 return "";
|
|
493 return db.ref;
|
|
494 }
|
|
495
|
|
496 const char *
|
|
497 Db_xref( const char *msgId )
|
|
498 {
|
|
499 if ( ! loadArt( msgId ) )
|
|
500 return "";
|
|
501 return db.xref;
|
|
502 }
|
|
503
|
|
504 Bool
|
|
505 Db_contains( const char *msgId )
|
|
506 {
|
|
507 datum key;
|
|
508
|
|
509 ASSERT( db.dbf );
|
|
510 if ( strcmp( msgId, db.msgId ) == 0 )
|
|
511 return TRUE;
|
|
512 key.dptr = (void*)msgId;
|
|
513 key.dsize = strlen( msgId ) + 1;
|
|
514 return gdbm_exists( db.dbf, key );
|
|
515 }
|
|
516
|
|
517 static datum cursor = { NULL, 0 };
|
|
518
|
|
519 Bool
|
|
520 Db_first( const char** msgId )
|
|
521 {
|
|
522 ASSERT( db.dbf );
|
|
523 if ( cursor.dptr != NULL )
|
|
524 {
|
|
525 free( cursor.dptr );
|
|
526 cursor.dptr = NULL;
|
|
527 }
|
|
528 cursor = gdbm_firstkey( db.dbf );
|
|
529 *msgId = cursor.dptr;
|
|
530 return ( cursor.dptr != NULL );
|
|
531 }
|
|
532
|
|
533 Bool
|
|
534 Db_next( const char** msgId )
|
|
535 {
|
|
536 void *oldDptr = cursor.dptr;
|
|
537
|
|
538 ASSERT( db.dbf );
|
|
539 if ( cursor.dptr == NULL )
|
|
540 return FALSE;
|
|
541 cursor = gdbm_nextkey( db.dbf, cursor );
|
|
542 free( oldDptr );
|
|
543 *msgId = cursor.dptr;
|
|
544 return ( cursor.dptr != NULL );
|
|
545 }
|
|
546
|
|
547 Bool
|
|
548 Db_expire( unsigned int days )
|
|
549 {
|
|
550 double limit;
|
|
551 int cntDel, cntLeft, flags;
|
|
552 time_t nowTime, lastAccess;
|
|
553 const char *msgId;
|
|
554 Str name, tmpName;
|
|
555 GDBM_FILE tmpDbf;
|
|
556 datum key, val;
|
|
557
|
|
558 if ( ! Db_open() )
|
|
559 return FALSE;
|
|
560 snprintf( name, MAXCHAR, "%s/data/articles.gdbm", Cfg_spoolDir() );
|
|
561 snprintf( tmpName, MAXCHAR, "%s/data/articles.gdbm.new", Cfg_spoolDir() );
|
|
562 flags = GDBM_NEWDB | GDBM_FAST;
|
|
563 if ( ! ( tmpDbf = gdbm_open( tmpName, 512, flags, 0644, NULL ) ) )
|
|
564 {
|
|
565 Log_err( "Error opening %s for read/write (%s)", errMsg() );
|
|
566 Db_close();
|
|
567 return FALSE;
|
|
568 }
|
|
569 Log_inf( "Expiring articles that have not been accessed for %u days",
|
|
570 days );
|
|
571 limit = days * 24. * 3600.;
|
|
572 cntDel = 0;
|
|
573 cntLeft = 0;
|
|
574 nowTime = time( NULL );
|
|
575 if ( Db_first( &msgId ) )
|
|
576 do
|
|
577 {
|
|
578 lastAccess = Db_lastAccess( msgId );
|
|
579 if ( lastAccess == -1 )
|
|
580 Log_err( "Internal error: Getting lastAccess of %s failed",
|
|
581 msgId );
|
|
582 else if ( difftime( nowTime, lastAccess ) > limit )
|
|
583 {
|
|
584 Log_dbg( "Expiring %s", msgId );
|
|
585 ++cntDel;
|
|
586 }
|
|
587 else
|
|
588 {
|
|
589 ++cntLeft;
|
|
590 key.dptr = (void *)msgId;
|
|
591 key.dsize = strlen( msgId ) + 1;
|
|
592
|
|
593 val = gdbm_fetch( db.dbf, key );
|
|
594 if ( val.dptr != NULL )
|
|
595 {
|
|
596 if ( gdbm_store( tmpDbf, key, val, GDBM_INSERT ) != 0 )
|
|
597 Log_err( "Could not store %s in new database (%s)",
|
|
598 errMsg() );
|
|
599 free( val.dptr );
|
|
600 }
|
|
601 }
|
|
602 }
|
|
603 while ( Db_next( &msgId ) );
|
|
604 Log_inf( "%lu articles deleted, %lu left", cntDel, cntLeft );
|
|
605 gdbm_close( tmpDbf );
|
|
606 Db_close();
|
|
607 rename( tmpName, name );
|
|
608 return TRUE;
|
|
609 }
|