E-MailRelay
gpopstore.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2021 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any 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, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file gpopstore.cpp
19///
20
21#include "gdef.h"
22#include "gpopstore.h"
23#include "gstr.h"
24#include "gfile.h"
25#include "gdirectory.h"
26#include "gtest.h"
27#include "groot.h"
28#include "gassert.h"
29#include <sstream>
30#include <fstream>
31
32namespace GPop
33{
34 struct FileReader ;
35 struct DirectoryReader ;
36 struct FileDeleter ;
37}
38
39//| \class GPop::FileReader
40/// A trivial class which is used like G::Root by GPop::Store for reading files.
41/// The implementation does nothing because files in the pop store are group-readable.
42///
44{
45 FileReader() ;
46} ;
47
48//| \class GPop::DirectoryReader
49/// A trivial class which is used like G::Root by GPop::Store for reading
50/// directory listings.
51///
53{
54 DirectoryReader() = default;
55} ;
56
57//| \class GPop::FileDeleter
58/// A trivial specialisation of G::Root used by GPop::Store for deleting files.
59/// The specialisation is not really necessary because the pop store directory
60/// is group-writeable.
61///
62struct GPop::FileDeleter : private G::Root
63{
64} ;
65
66// ==
67
68GPop::Store::Store( const G::Path & path , bool by_name , bool allow_delete ) :
69 m_path(path) ,
70 m_by_name(by_name) ,
71 m_allow_delete(allow_delete)
72{
73 checkPath( path , by_name , allow_delete ) ;
74}
75
77{
78 return m_path ;
79}
80
82{
83 return m_allow_delete ;
84}
85
87{
88 return m_by_name ;
89}
90
91void GPop::Store::checkPath( const G::Path & dir_path , bool by_name , bool allow_delete )
92{
93 if( by_name )
94 {
95 if( !valid(dir_path,false) )
96 throw InvalidDirectory() ;
97
98 G::DirectoryList iter ;
99 {
100 DirectoryReader claim_reader ;
101 iter.readAll( dir_path ) ;
102 }
103
104 int n = 0 ;
105 while( iter.more() )
106 {
107 if( iter.isDir() )
108 {
109 n++ ;
110 if( !valid(iter.filePath(),allow_delete) )
111 {
112 ; // no-op -- warning only
113 }
114 }
115 }
116 if( n == 0 )
117 {
118 G_WARNING( "GPop::Store: no sub-directories for pop-by-name found in \"" << dir_path << "\": "
119 << "create one sub-directory for each authorised pop account" ) ;
120 }
121 }
122 else if( !valid(dir_path,allow_delete) )
123 {
124 throw InvalidDirectory() ;
125 }
126}
127
128bool GPop::Store::valid( const G::Path & dir_path , bool allow_delete )
129{
130 G::Directory dir_test( dir_path ) ;
131 bool ok = false ;
132 if( allow_delete )
133 {
134 std::string tmp_filename = G::Directory::tmp() ;
135 FileDeleter claim_deleter ;
136 ok = dir_test.valid() && dir_test.writeable( tmp_filename ) ;
137 }
138 else
139 {
140 FileReader claim_reader ;
141 ok = dir_test.valid() ;
142 }
143 if( !ok )
144 {
145 const char * op = allow_delete ? "writing" : "reading" ;
146 G_WARNING( "GPop::Store: directory not valid for " << op << ": \"" << dir_path << "\"" ) ;
147 }
148 return ok ;
149}
150
151// ===
152
153GPop::StoreLock::File::File( const G::Path & content_path ) :
154 name(content_path.basename()) ,
155 size(toSize(G::File::sizeString(content_path.str())))
156{
157}
158
159GPop::StoreLock::File::File( const std::string & content_name , const std::string & size_string ) :
160 name(content_name) ,
161 size(toSize(size_string))
162{
163}
164
165bool GPop::StoreLock::File::operator<( const File & rhs ) const
166{
167 return name < rhs.name ;
168}
169
170GPop::StoreLock::Size GPop::StoreLock::File::toSize( const std::string & s )
171{
172 return G::Str::toULong( s , G::Str::Limited() ) ;
173}
174
175// ===
176
178 m_store(&store)
179{
180}
181
182void GPop::StoreLock::lock( const std::string & user )
183{
184 G_ASSERT( ! locked() ) ;
185 G_ASSERT( ! user.empty() ) ;
186 G_ASSERT( m_store != nullptr ) ;
187
188 m_user = user ;
189 m_dir = m_store->dir() ;
190 if( m_store->byName() )
191 m_dir.pathAppend( user ) ;
192
193 // build a read-only list of files (inc. file sizes)
194 {
195 DirectoryReader claim_reader ;
196 G::DirectoryList iter ;
197 iter.readType( m_dir , ".envelope" ) ;
198 while( iter.more() )
199 {
200 File file( contentPath(iter.fileName()) ) ;
201 m_initial.insert( file ) ;
202 }
203 }
204
205 if( G::Test::enabled("large-pop-list") )
206 {
207 // create a larger list
208 std::size_t limit = m_initial.size() * 1000U ;
209 for( std::size_t i = 0U ; i < limit ; i++ )
210 {
211 std::ostringstream ss ;
212 ss << "dummy." << i << ".content" ;
213 m_initial.insert( File(ss.str()) ) ;
214 }
215 }
216
217 // take a mutable copy
218 m_current = m_initial ;
219
220 G_ASSERT( locked() ) ;
221}
222
224{
225 return m_store != nullptr && ! m_user.empty() ;
226}
227
229= default;
230
231GPop::StoreLock::Size GPop::StoreLock::messageCount() const
232{
233 G_ASSERT( locked() ) ;
234 return static_cast<Size>(m_current.size()) ;
235}
236
237GPop::StoreLock::Size GPop::StoreLock::totalByteCount() const
238{
239 G_ASSERT( locked() ) ;
240 Size total = 0 ;
241 for( const auto & item : m_current )
242 total += item.size ;
243 return total ;
244}
245
246bool GPop::StoreLock::valid( int id ) const
247{
248 G_ASSERT( locked() ) ;
249 return id >= 1 && id <= static_cast<int>(m_initial.size()) ;
250}
251
252GPop::StoreLock::Set::iterator GPop::StoreLock::find( int id )
253{
254 G_ASSERT( valid(id) ) ;
255 auto initial_p = m_initial.begin() ;
256 for( int i = 1 ; i < id && initial_p != m_initial.end() ; i++ , ++initial_p ) ;
257 return initial_p ;
258}
259
260GPop::StoreLock::Set::const_iterator GPop::StoreLock::find( int id ) const
261{
262 G_ASSERT( valid(id) ) ;
263 auto initial_p = m_initial.begin() ;
264 for( int i = 1 ; i < id && initial_p != m_initial.end() ; i++ , ++initial_p ) ;
265 return initial_p ;
266}
267
268GPop::StoreLock::Set::iterator GPop::StoreLock::find( const std::string & name )
269{
270 auto current_p = m_current.begin() ;
271 for( ; current_p != m_current.end() ; ++current_p )
272 {
273 if( (*current_p).name == name )
274 break ;
275 }
276 return current_p ;
277}
278
279GPop::StoreLock::Size GPop::StoreLock::byteCount( int id ) const
280{
281 G_ASSERT( locked() ) ;
282 return (*find(id)).size ;
283}
284
285GPop::StoreLock::List GPop::StoreLock::list( int id ) const
286{
287 G_ASSERT( locked() ) ;
288 List list ;
289 int i = 1 ;
290 for( auto p = m_current.begin() ; p != m_current.end() ; ++p , i++ )
291 {
292 if( id == -1 || id == i )
293 list.push_back( Entry(i,(*p).size,(*p).name) ) ;
294 }
295 return list ;
296}
297
298std::unique_ptr<std::istream> GPop::StoreLock::get( int id ) const
299{
300 G_ASSERT( locked() ) ;
301 G_ASSERT( valid(id) ) ;
302
303 G_DEBUG( "GPop::StoreLock::get: " << id << ": " << path(id) ) ;
304
305 auto file = std::make_unique<std::ifstream>() ;
306 G::Path file_path = path( id ) ;
307 {
308 FileReader claim_reader ;
309 G::File::open( *file , file_path ) ;
310 }
311
312 if( !file->good() )
313 throw CannotRead( file_path.str() ) ;
314
315 return std::unique_ptr<std::istream>( file.release() ) ;
316}
317
319{
320 G_ASSERT( locked() ) ;
321 G_ASSERT( valid(id) ) ;
322
323 auto initial_p = find( id ) ;
324 auto current_p = find( (*initial_p).name ) ;
325 if( current_p != m_current.end() )
326 {
327 m_deleted.insert( *initial_p ) ;
328 m_current.erase( current_p ) ;
329 }
330}
331
333{
334 G_ASSERT( locked() ) ;
335 if( m_store )
336 {
337 Store * store = m_store ;
338 m_store = nullptr ;
339 doCommit( *store ) ;
340 }
341 m_store = nullptr ;
342}
343
344void GPop::StoreLock::doCommit( Store & store ) const
345{
346 bool all_ok = true ;
347 for( const auto & item : m_deleted )
348 {
349 if( store.allowDelete() )
350 {
351 deleteFile( envelopePath(item) , all_ok ) ;
352 if( unlinked(store,item) ) // race condition could leave content files undeleted
353 deleteFile( contentPath(item) , all_ok ) ;
354 }
355 else
356 {
357 G_DEBUG( "StoreLock::doCommit: not deleting \"" << item.name << "\"" ) ;
358 }
359 }
360 if( ! all_ok )
361 throw CannotDelete() ;
362}
363
364void GPop::StoreLock::deleteFile( const G::Path & path , bool & all_ok ) const
365{
366 bool ok = false ;
367 {
368 FileDeleter claim_deleter ;
369 ok = G::File::remove( path , std::nothrow ) ;
370 }
371 all_ok = ok && all_ok ;
372 if( ! ok )
373 G_ERROR( "StoreLock::remove: failed to delete " << path ) ;
374}
375
376std::string GPop::StoreLock::uidl( int id ) const
377{
378 G_ASSERT( valid(id) ) ;
379 auto p = find( id ) ;
380 return (*p).name ;
381}
382
383G::Path GPop::StoreLock::path( int id ) const
384{
385 G_ASSERT( valid(id) ) ;
386 auto p = find( id ) ;
387 const File & file = (*p) ;
388 return contentPath( file ) ;
389}
390
391G::Path GPop::StoreLock::path( const std::string & filename , bool fallback ) const
392{
393 // expected path
394 G::Path path_1 = m_dir ;
395 path_1.pathAppend( filename ) ;
396
397 // or fallback to the parent directory
398 G::Path path_2 = m_dir ; path_2.pathAppend("..") ;
399 path_2.pathAppend( filename ) ;
400
401 return ( fallback && !G::File::exists(path_1,std::nothrow) ) ? path_2 : path_1 ;
402}
403
404std::string GPop::StoreLock::envelopeName( const std::string & content_name ) const
405{
406 std::string filename = content_name ;
407 G::Str::replace( filename , "content" , "envelope" ) ;
408 return filename ;
409}
410
411std::string GPop::StoreLock::contentName( const std::string & envelope_name ) const
412{
413 std::string filename = envelope_name ;
414 G::Str::replace( filename , "envelope" , "content" ) ;
415 return filename ;
416}
417
418G::Path GPop::StoreLock::contentPath( const std::string & envelope_name ) const
419{
420 const bool try_parent_directory = true ;
421 return path( contentName(envelope_name) , try_parent_directory ) ;
422}
423
424G::Path GPop::StoreLock::contentPath( const File & file ) const
425{
426 const bool try_parent_directory = true ;
427 return path( file.name , try_parent_directory ) ;
428}
429
430G::Path GPop::StoreLock::envelopePath( const File & file ) const
431{
432 const bool try_parent_directory = false ;
433 return path( envelopeName(file.name) , try_parent_directory ) ;
434}
435
437{
438 G_ASSERT( locked() ) ;
439 m_deleted.clear() ;
440 m_current = m_initial ;
441}
442
443bool GPop::StoreLock::unlinked( Store & store , const File & file ) const
444{
445 if( !store.byName() )
446 {
447 G_DEBUG( "StoreLock::unlinked: unlinked since not pop-by-name: " << file.name ) ;
448 return true ;
449 }
450
451 G::Path normal_content_path = m_dir ; normal_content_path.pathAppend( file.name ) ;
452 if( G::File::exists(normal_content_path,std::nothrow) )
453 {
454 G_DEBUG( "StoreLock::unlinked: unlinked since in its own directory: " << normal_content_path ) ;
455 return true ;
456 }
457
458 // look for corresponding envelopes in all child directories
459 bool found = false ;
460 {
461 G::DirectoryList iter ;
462 {
463 DirectoryReader claim_reader ;
464 iter.readAll( store.dir() ) ;
465 }
466 while( iter.more() )
467 {
468 if( ! iter.isDir() ) continue ;
469 G_DEBUG( "Store::unlinked: checking sub-directory: " << iter.fileName() ) ;
470 G::Path envelope_path = iter.filePath() ; envelope_path.pathAppend(envelopeName(file.name)) ;
471 if( G::File::exists(envelope_path,std::nothrow) )
472 {
473 G_DEBUG( "StoreLock::unlinked: still in use: envelope exists: " << envelope_path ) ;
474 found = true ;
475 break ;
476 }
477 }
478 }
479
480 if( ! found )
481 {
482 G_DEBUG( "StoreLock::unlinked: unlinked since no envelope found in any sub-directory" ) ;
483 return true ;
484 }
485
486 return false ;
487}
488
489GPop::FileReader::FileReader() // NOLINT modernize-use-equals-default because of -Wunused
490{
491}
492
Represents a file in the GPop::Store.
Definition: gpopstore.h:84
Size byteCount(int id) const
Returns a message size.
Definition: gpopstore.cpp:279
bool valid(int id) const
Validates a message number.
Definition: gpopstore.cpp:246
std::string uidl(int id) const
Returns a message's unique id.
Definition: gpopstore.cpp:376
std::unique_ptr< std::istream > get(int id) const
Retrieves the message content.
Definition: gpopstore.cpp:298
List list(int id=-1) const
Lists messages in the store.
Definition: gpopstore.cpp:285
void rollback()
Rolls back remove()als but retains the lock.
Definition: gpopstore.cpp:436
void commit()
Commits remove()als.
Definition: gpopstore.cpp:332
~StoreLock()
Destructor.
void lock(const std::string &user)
Initialisation.
Definition: gpopstore.cpp:182
StoreLock(Store &store)
Constructor.
Definition: gpopstore.cpp:177
Size messageCount() const
Returns the store's message count.
Definition: gpopstore.cpp:231
void remove(int)
Marks the message for removal.
Definition: gpopstore.cpp:318
Size totalByteCount() const
Returns the store's total byte count.
Definition: gpopstore.cpp:237
bool locked() const
Returns true if locked.
Definition: gpopstore.cpp:223
A message store.
Definition: gpopstore.h:45
bool allowDelete() const
Returns true if files can be deleted.
Definition: gpopstore.cpp:81
bool byName() const
Returns true if the spool directory is affected by the user name.
Definition: gpopstore.cpp:86
G::Path dir() const
Returns the spool directory path.
Definition: gpopstore.cpp:76
Store(const G::Path &spool_dir, bool by_name, bool allow_delete)
Constructor.
Definition: gpopstore.cpp:68
A iterator similar to G::DirectoryIterator but doing all file i/o in one go.
Definition: gdirectory.h:146
Path filePath() const
Returns the current path.
Definition: gdirectory.cpp:133
void readType(const Path &dir, const std::string &suffix, unsigned int limit=0U)
An initialiser that is to be used after default construction.
Definition: gdirectory.cpp:88
bool more()
Returns true if more and advances by one.
Definition: gdirectory.cpp:112
std::string fileName() const
Returns the current filename.
Definition: gdirectory.cpp:138
void readAll(const Path &dir)
An initialiser that is to be used after default construction.
Definition: gdirectory.cpp:83
bool isDir() const
Returns true if the current item is a directory.
Definition: gdirectory.cpp:128
An encapsulation of a file system directory that works with G::DirectoryIterator.
Definition: gdirectory.h:46
static std::string tmp()
A convenience function for constructing a filename for writeable().
Definition: gdirectory.cpp:57
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:55
static bool remove(const Path &path, std::nothrow_t) noexcept
Deletes the file or directory. Returns false on error.
Definition: gfile.cpp:29
static bool exists(const Path &file)
Returns true if the file (directory, device etc.) exists.
Definition: gfile.cpp:151
A Path object represents a file system path.
Definition: gpath.h:72
void pathAppend(const std::string &tail)
Appends a filename or a relative path to this path.
Definition: gpath.cpp:375
std::string str() const
Returns the path string.
Definition: gpath.h:215
A class which acquires the process's special privileges on construction and releases them on destruct...
Definition: groot.h:52
static unsigned long toULong(const std::string &s, Limited)
Converts string 's' to an unsigned long.
Definition: gstr.cpp:626
static bool replace(std::string &s, const std::string &from, const std::string &to, std::size_t *pos_p=nullptr)
Replaces 'from' with 'to', starting at offset '*pos_p'.
Definition: gstr.cpp:264
static bool enabled() noexcept
Returns true if test features are enabled.
Definition: gtest.cpp:79
POP3 classes.
Definition: gpopserver.h:37
Low-level classes.
Definition: galign.h:28
A trivial class which is used like G::Root by GPop::Store for reading directory listings.
Definition: gpopstore.cpp:53
A trivial specialisation of G::Root used by GPop::Store for deleting files.
Definition: gpopstore.cpp:63
A trivial class which is used like G::Root by GPop::Store for reading files.
Definition: gpopstore.cpp:44
Overload discrimiator for G::Str::toUWhatever() requesting a range-limited result.
Definition: gstr.h:53