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