E-MailRelay
gpopstore.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2023 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 <algorithm>
30#include <iterator>
31#include <sstream>
32#include <fstream>
33
34namespace GPop
35{
36 namespace StoreImp
37 {
38 struct FileReader /// Used by GPop::Store like G::Root when reading.
39 {
40 FileReader() ;
41 } ;
42 struct DirectoryReader : private G::Root /// Used by GPop::Store like G::Root when reading a directory.
43 {
44 } ;
45 struct FileDeleter : private G::Root /// Used by GPop::Store like G::Root when deleting.
46 {
47 } ;
48 unsigned long toSize( const std::string & s )
49 {
50 return (s.empty() || !G::Str::isULong(s) ) ? 0UL : G::Str::toULong(s,G::Str::Limited()) ;
51 }
52 }
53}
54
55// ==
56
57GPop::Store::Store( const G::Path & path , bool by_name , bool allow_delete ) :
58 m_path(path) ,
59 m_by_name(by_name) ,
60 m_allow_delete(allow_delete)
61{
62 if( !accessible( path , by_name?false:allow_delete ) )
63 throw InvalidDirectory() ;
64
65 if( by_name )
66 {
67 // read the spool directory
68 G::DirectoryList iter ;
69 {
70 StoreImp::DirectoryReader claim_reader ;
71 iter.readAll( path ) ;
72 }
73
74 // check every sub-directory is accessible() and count them
75 int n = 0 ;
76 while( iter.more() )
77 {
78 if( iter.isDir() )
79 {
80 bool ok = accessible( iter.filePath() , allow_delete ) ;
81 G_DEBUG( "GPop::Store::ctor: [" << iter.filePath() << "]: " << (ok?"good":"bad") ) ;
82 if( ok )
83 n++ ;
84 }
85 }
86
87 // warn if no accessible() sub-directories
88 if( n == 0 )
89 {
90 G_WARNING( "GPop::Store::ctor: no sub-directories for pop-by-name found in \"" << path << "\": "
91 << "create one sub-directory for each authorised pop account" ) ;
92 }
93 }
94}
95
96bool GPop::Store::accessible( const G::Path & dir_path , bool for_write )
97{
98 G::Directory dir_test( dir_path ) ;
99 bool ok = false ;
100 if( for_write )
101 {
102 std::string tmp_filename = G::Directory::tmp() ;
103 StoreImp::FileDeleter claim_deleter ;
104 ok = dir_test.valid() && dir_test.writeable( tmp_filename ) ;
105 }
106 else
107 {
108 StoreImp::FileReader claim_reader ;
109 ok = dir_test.valid() ;
110 }
111 if( !ok )
112 {
113 const char * op = for_write ? "writing" : "reading" ;
114 G_WARNING( "GPop::Store: directory not valid for " << op << ": \"" << dir_path << "\"" ) ;
115 }
116 return ok ;
117}
118
120{
121 return m_path ;
122}
123
125{
126 return m_allow_delete ;
127}
128
130{
131 return m_by_name ;
132}
133
134// ===
135
136GPop::StoreMessage::StoreMessage( const std::string & name_in , Size size_in , bool in_parent_in ) :
137 name(name_in) ,
138 size(size_in) ,
139 in_parent(in_parent_in) ,
140 deleted(false)
141{
142}
143
144G::Path GPop::StoreMessage::cpath( const G::Path & edir , const G::Path & sdir ) const
145{
146 return in_parent ? cpath(sdir) : cpath(edir) ;
147}
148
149G::Path GPop::StoreMessage::cpath( const G::Path & dir ) const
150{
151 return G::Path( dir , name+".content" ) ;
152}
153
154G::Path GPop::StoreMessage::epath( const G::Path & edir ) const
155{
156 return G::Path( edir , name+".envelope" ) ;
157}
158
159GPop::StoreMessage GPop::StoreMessage::invalid()
160{
161 return {{},0,false} ;
162}
163
164std::string GPop::StoreMessage::uidl() const
165{
166 return name + ".content" ;
167}
168
169// ===
170
171GPop::StoreUser::StoreUser( Store & store , const std::string & user ) :
172 m_store(store) ,
173 m_user(user) ,
174 m_edir(store.byName()?G::Path(store.dir(),user):store.dir()) ,
175 m_sdir(store.dir())
176{
177 G_ASSERT( !user.empty() ) ;
178
179 // build a list of envelope files, with content file sizes
180 {
181 StoreImp::DirectoryReader claim_reader ;
182 G::DirectoryList iter ;
183 std::size_t n = iter.readType( m_edir , ".envelope" ) ;
184 m_list.reserve( n ) ;
185 while( iter.more() )
186 {
187 std::string ename = iter.fileName() ;
188 std::string name = G::Str::head( ename , ename.rfind('.') ) ;
189 std::string cname = name + ".content" ;
190
191 bool in_parent =
192 !G::File::exists( G::Path(m_edir,cname) , std::nothrow ) &&
193 m_store.byName() &&
194 G::File::exists( G::Path(m_sdir,cname) , std::nothrow ) ;
195
196 G::Path cpath = in_parent ? G::Path(m_sdir,cname) : G::Path(m_edir,cname) ;
197
198 auto csize = StoreImp::toSize( G::File::sizeString(cpath) ) ;
199 if( csize )
200 m_list.emplace_back( name , csize , in_parent ) ;
201 }
202 }
203}
204
205// ==
206
208= default ;
209
210GPop::StoreList::StoreList( const StoreUser & store_user , bool allow_delete ) :
211 m_allow_delete(allow_delete) ,
212 m_edir(store_user.m_edir) ,
213 m_sdir(store_user.m_sdir) ,
214 m_list(store_user.m_list)
215{
216}
217
218GPop::StoreList::List::const_iterator GPop::StoreList::cbegin() const
219{
220 return m_list.cbegin() ;
221}
222
223GPop::StoreList::List::const_iterator GPop::StoreList::cend() const
224{
225 return m_list.cend() ;
226}
227
228GPop::StoreMessage::Size GPop::StoreList::messageCount() const
229{
230 std::size_t n = std::count_if( m_list.begin() , m_list.end() ,
231 [](const StoreMessage &msg_){return !msg_.deleted;} ) ;
232
233 return static_cast<StoreMessage::Size>(n) ;
234}
235
236GPop::StoreList::Size GPop::StoreList::totalByteCount() const
237{
238 Size total = 0 ;
239 for( const auto & item : m_list )
240 {
241 if( !item.deleted )
242 total += item.size ;
243 }
244 return total ;
245}
246
247bool GPop::StoreList::valid( int id ) const
248{
249 return id >= 1 && id <= static_cast<int>(m_list.size()) && !m_list.at(id-1).deleted ;
250}
251
253{
254 G_ASSERT( valid(id) ) ;
255 return valid(id) ? m_list.at(id-1) : StoreMessage::invalid() ;
256}
257
258GPop::StoreList::Size GPop::StoreList::byteCount( int id ) const
259{
260 G_ASSERT( valid(id) ) ;
261 return valid(id) ? m_list.at(id-1).size : Size(0) ;
262}
263
264std::unique_ptr<std::istream> GPop::StoreList::content( int id ) const
265{
266 G_ASSERT( valid(id) ) ;
267 if( !valid(id) ) throw CannotRead( std::to_string(id) ) ;
268
269 G::Path cpath = m_list.at(id-1).cpath(m_edir,m_sdir) ;
270 G_DEBUG( "GPop::StoreList::get: " << id << " " << cpath ) ;
271
272 auto fstream = std::make_unique<std::ifstream>() ;
273 {
274 StoreImp::FileReader claim_reader ;
275 G::File::open( *fstream , cpath ) ;
276 }
277
278 if( !fstream->good() )
279 throw CannotRead( cpath.str() ) ;
280
281 return fstream ;
282}
283
285{
286 if( valid(id) )
287 m_list.at(id-1).deleted = true ;
288}
289
291{
292 bool all_ok = true ;
293 for( const auto & item : m_list )
294 {
295 if( item.deleted && m_allow_delete )
296 {
297 deleteFile( item.epath(m_edir) , all_ok ) ;
298 if( !shared(item) ) // race condition could leave content files undeleted
299 deleteFile( item.cpath(m_edir,m_sdir) , all_ok ) ;
300 }
301 else if( item.deleted )
302 {
303 G_DEBUG( "StoreList::doCommit: not deleting \"" << item.name << "\"" ) ;
304 }
305 }
306 if( ! all_ok )
307 throw CannotDelete() ;
308}
309
310void GPop::StoreList::deleteFile( const G::Path & path , bool & all_ok ) const
311{
312 bool ok = false ;
313 {
314 StoreImp::FileDeleter claim_deleter ;
315 ok = G::File::remove( path , std::nothrow ) ;
316 }
317 all_ok = ok && all_ok ;
318 if( ! ok )
319 G_ERROR( "StoreList::deleteFile: failed to delete " << path ) ;
320}
321
322#ifndef G_LIB_SMALL
323std::string GPop::StoreList::uidl( int id ) const
324{
325 G_ASSERT( valid(id) ) ;
326 return valid(id) ? m_list.at(id-1).uidl() : std::string() ;
327}
328#endif
329
331{
332 for( auto & item : m_list )
333 item.deleted = false ;
334}
335
336bool GPop::StoreList::shared( const StoreMessage & message ) const
337{
338 if( !message.in_parent )
339 {
340 return false ;
341 }
342 else
343 {
344 // look for envelopes that share this content
345 G_DEBUG( "GPop::StoreList::shared: test sharing of " << message.cpath(m_edir,m_sdir) ) ;
346
347 // start with the main spool directory
348 bool found = G::File::exists( message.epath(m_sdir) , std::nothrow ) ;
349 G_DEBUG_IF( found , "GPop::StoreList::shared: content shared: envelope: " << message.epath(m_sdir) ) ;
350
351 // and then sub-directories
352 G::DirectoryList iter ;
353 {
354 StoreImp::DirectoryReader claim_reader ;
355 iter.readAll( m_sdir ) ;
356 }
357 while( iter.more() && !found )
358 {
359 if( !iter.isDir() )
360 continue ;
361
362 G::Path sub_dir = iter.filePath() ;
363 G_DEBUG( "GPop::StoreList::shared: checking sub-directory: " << sub_dir ) ;
364
365 G::Path epath( message.epath(m_sdir+sub_dir.basename()) ) ;
366 found = G::File::exists( epath , std::nothrow ) ;
367 G_DEBUG_IF( found , "GPop::StoreList::shared: content shared: envelope: " << epath ) ;
368 }
369 G_DEBUG_IF( !found , "GPop::StoreList::shared: content not shared: no matching envelope" ) ;
370
371 return found ;
372 }
373}
374
375GPop::StoreImp::FileReader::FileReader() // NOLINT modernize-use-equals-default because of -Wunused
376{
377}
378
Size messageCount() const
Returns the store's message count.
Definition: gpopstore.cpp:228
std::string uidl(int id) const
Returns a message's unique 1-based id.
Definition: gpopstore.cpp:323
void rollback()
Rolls back remove()als.
Definition: gpopstore.cpp:330
Size byteCount(int id) const
Returns the message size.
Definition: gpopstore.cpp:258
bool valid(int id) const
Validates a message id.
Definition: gpopstore.cpp:247
StoreList()
Constructor for an empty list.
List::const_iterator cbegin() const
Returns the begin iterator.
Definition: gpopstore.cpp:218
std::unique_ptr< std::istream > content(int id) const
Retrieves the message content.
Definition: gpopstore.cpp:264
Size totalByteCount() const
Returns the store's total byte count.
Definition: gpopstore.cpp:236
void remove(int)
Marks the message files for deletion.
Definition: gpopstore.cpp:284
void commit()
Commits remove()als.
Definition: gpopstore.cpp:290
List::const_iterator cend() const
Returns the end iterator.
Definition: gpopstore.cpp:223
List::value_type get(int id) const
Returns the item with index id-1.
Definition: gpopstore.cpp:252
A structure representing a pop message.
Definition: gpopstore.h:84
Holds the list of messages available to a particular pop user.
Definition: gpopstore.h:105
StoreUser(Store &, const std::string &user)
Constructor.
Definition: gpopstore.cpp:171
A message store.
Definition: gpopstore.h:47
bool allowDelete() const
Returns true if files can be deleted.
Definition: gpopstore.cpp:124
bool byName() const
Returns true if the spool directory is affected by the user name.
Definition: gpopstore.cpp:129
G::Path dir() const
Returns the spool directory path.
Definition: gpopstore.cpp:119
Store(const G::Path &spool_dir, bool by_name, bool allow_delete)
Constructor. Throws InvalidDirectory.
Definition: gpopstore.cpp:57
A iterator similar to G::DirectoryIterator but doing all file i/o in one go and providing a sorted re...
Definition: gdirectory.h:148
Path filePath() const
Returns the current path.
Definition: gdirectory.cpp:152
std::size_t readAll(const Path &dir)
An initialiser that is to be used after default construction.
Definition: gdirectory.cpp:82
bool more()
Returns true if more and advances by one.
Definition: gdirectory.cpp:124
std::string fileName() const
Returns the current filename.
Definition: gdirectory.cpp:157
std::size_t readType(const Path &dir, string_view suffix, unsigned int limit=0U)
An initialiser that is to be used after default construction.
Definition: gdirectory.cpp:94
bool isDir() const
Returns true if the current item is a directory.
Definition: gdirectory.cpp:147
An encapsulation of a file system directory that works with G::DirectoryIterator.
Definition: gdirectory.h:47
static std::string tmp()
A convenience function for constructing a filename for writeable().
Definition: gdirectory.cpp:55
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:55
static std::string sizeString(const Path &file)
Returns the file's size in string format.
Definition: gfile.cpp:209
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:148
A Path object represents a file system path.
Definition: gpath.h:73
std::string basename() const
Returns the rightmost part of the path, ignoring "." parts.
Definition: gpath.cpp:346
std::string str() const
Returns the path string.
Definition: gpath.h:224
A class which acquires the process's special privileges on construction and releases them on destruct...
Definition: groot.h:52
static bool isULong(string_view s) noexcept
Returns true if the string can be converted into an unsigned long without throwing an exception.
Definition: gstr.cpp:457
static unsigned long toULong(string_view s, Limited)
Converts string 's' to an unsigned long.
Definition: gstr.cpp:672
static std::string head(string_view in, std::size_t pos, string_view default_={})
Returns the first part of the string up to just before the given position.
Definition: gstr.cpp:1297
POP3 classes.
Definition: gpop.h:33
Low-level classes.
Definition: garg.h:30
Overload discrimiator for G::Str::toUWhatever() requesting a range-limited result.
Definition: gstr.h:56