E-MailRelay
gsplitfilter.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 gsplitfilter.cpp
19///
20
21#include "gdef.h"
22#include "gsplitfilter.h"
23#include "gstoredfile.h"
24#include "gprocess.h"
25#include "gstringview.h"
26#include "gstringtoken.h"
27#include "gexception.h"
28#include "gscope.h"
29#include "gassert.h"
30#include "glog.h"
31#include <algorithm>
32#include <iterator>
33
35 Filter::Type filter_type , const Filter::Config & filter_config , const std::string & spec ) :
36 SimpleFilterBase(es,filter_type,"split:") ,
37 m_store(store) ,
38 m_filter_config(filter_config)
39{
40 std::string_view spec_sv = spec ;
41 for( G::StringTokenView t(spec_sv,";",1U) ; t ; ++t )
42 {
43 if( t() == "raw" ) m_raw = true ; // case-sensitive domain names
44 if( G::Str::isNumeric(t()) ) m_port = G::sv_to_string(t()) ;
45 }
46}
47
48GSmtp::Filter::Result GFilters::SplitFilter::run( const GStore::MessageId & message_id ,
49 bool & , GStore::FileStore::State e_state )
50{
51 G::Path content_path = m_store.contentPath( message_id ) ;
52 G::Path envelope_path = m_store.envelopePath( message_id , e_state ) ;
53
54 GStore::Envelope envelope = GStore::FileStore::readEnvelope( envelope_path ) ;
55
56 // group-by domain
57 G::StringArray domains ;
58 std::transform( envelope.to_remote.begin() , envelope.to_remote.end() ,
59 std::back_inserter(domains) , [](std::string &to_){return G::Str::tail(to_,"@");} ) ;
60 bool raw = m_raw ;
61 std::for_each( domains.begin() , domains.end() , [raw](std::string &to_){normalise(to_,raw);} ) ;
62 std::sort( domains.begin() , domains.end() ) ;
63 domains.erase( std::unique(domains.begin(),domains.end()) , domains.end() ) ;
64 if( domains.empty() )
65 {
66 G_LOG( "GFilters::SplitFilter::start: " << prefix() << ": no remote domains: nothing to do" ) ;
67 return Result::ok ;
68 }
69
70 // assign a message-id per domain
71 G::StringArray ids ;
72 ids.reserve( domains.size() ) ;
73 ids.push_back( message_id.str() ) ;
74 for( std::size_t i = 1U ; i < domains.size() ; i++ )
75 ids.push_back( m_store.newId().str() ) ;
76
77 // prepare extra headers giving the message ids of the split group
78 std::stringstream extra_headers ;
79 if( ids.size() > 1U )
80 {
81 extra_headers << GStore::FileStore::x() << "SplitGroupCount: " << ids.size() << "\n" ;
82 for( const auto & id : ids )
83 extra_headers << GStore::FileStore::x() << "SplitGroup: " << id << "\n" ;
84 }
85
86 // create new messages for each domain
87 for( std::size_t i = 1U ; i < domains.size() ; i++ )
88 {
89 std::string domain = domains[i] ;
90 GStore::MessageId new_id( ids[i] ) ;
91 G::StringArray recipients = matching( envelope.to_remote , domain ) ;
92 G_ASSERT( !recipients.empty() ) ;
93
94 G::Path new_content_path = m_store.contentPath( new_id ) ;
95 G::Path new_envelope_path = m_store.envelopePath( new_id ) ;
96
97 G_LOG( "GFilters::SplitFilter::start: " << prefix() << " creating "
98 << "[" << new_id.str() << "]: forward-to=[" << domain << "]" ) ;
99
100 GStore::Envelope new_envelope = envelope ;
101 new_envelope.to_local.clear() ;
102 new_envelope.to_remote = recipients ;
103 new_envelope.forward_to = forwardTo( recipients.at(0U) ) ;
104
105 if( !FileOp::hardlink( content_path , new_content_path ) )
106 throw G::Exception( "split: cannot copy content file" ,
107 new_content_path.str() , G::Process::strerror(FileOp::errno_()) ) ;
108 G::ScopeExit clean_up_content( [new_content_path](){FileOp::remove(new_content_path);} ) ;
109
110 std::ofstream new_envelope_stream ;
111 FileOp::openOut( new_envelope_stream , new_envelope_path ) ;
112 GStore::Envelope::write( new_envelope_stream , new_envelope ) ;
113 GStore::Envelope::copyExtra( extra_headers , new_envelope_stream ) ;
114 extra_headers.clear() ;
115 extra_headers.seekg( 0 ) ;
116
117 new_envelope_stream.close() ;
118 if( !new_envelope_stream )
119 throw G::Exception( "split: cannot create envelope file" ,
120 new_envelope_path.str() , G::Process::strerror(FileOp::errno_()) ) ;
121
122 clean_up_content.release() ;
123 }
124
125 // update the original message
126 G_LOG( "GFilters::SplitFilter::start: " << prefix() << " updating "
127 << "[" << message_id.str() << "]: forward-to=[" << domains[0] << "]" ) ;
128 G_ASSERT( !domains.empty() ) ;
129 G::StringArray recipients = matching( envelope.to_remote , domains.at(0) ) ;
130 G_ASSERT( !recipients.empty() ) ;
131 std::string forward_to = forwardTo( recipients.at(0U) ) ;
132 GStore::StoredFile msg( m_store , message_id , GStore::StoredFile::State::New ) ;
133 msg.editEnvelope( [forward_to,&recipients](GStore::Envelope &env_){
134 env_.to_remote = recipients ;
135 env_.forward_to = forward_to ;
136 } , &extra_headers ) ;
137
138 return Result::ok ;
139}
140
141G::StringArray GFilters::SplitFilter::matching( const G::StringArray & recipients , const std::string & domain ) const
142{
143 G::StringArray result ;
144 bool raw = m_raw ;
145 std::copy_if( recipients.begin() , recipients.end() , std::back_inserter(result) ,
146 [domain,raw](const std::string &to_){return match(G::Str::tail(to_,"@"),domain,raw) ;} ) ;
147 return result ;
148}
149
150bool GFilters::SplitFilter::match( const std::string & a , const std::string & b , bool raw )
151{
152 return raw ? a == b : G::Str::imatch( a , b ) ;
153}
154
155void GFilters::SplitFilter::normalise( std::string & domain , bool raw )
156{
157 if( !raw )
158 G::Str::toLower( domain ) ;
159}
160
161std::string GFilters::SplitFilter::forwardTo( const std::string & recipient ) const
162{
163 // user@example.com -> example.com:25
164 return G::Str::tail(recipient,"@").append(m_port.empty()?0U:1U,':').append(m_port) ;
165}
A GSmtp::Filter base class for filters that run synchronously.
SplitFilter(GNet::EventState es, GStore::FileStore &, Filter::Type, const Filter::Config &, const std::string &spec)
Constructor.
A lightweight object containing an ExceptionHandler pointer, optional ExceptionSource pointer and opt...
Definition: geventstate.h:131
A structure containing the contents of an envelope file, with support for file reading,...
Definition: genvelope.h:41
static void copyExtra(std::istream &, std::ostream &)
A convenience function to copy extra envelope lines from an envelope input stream to an output stream...
Definition: genvelope.cpp:99
static std::size_t write(std::ostream &, const Envelope &)
Writes an envelope to a seekable stream.
Definition: genvelope.cpp:59
A concrete implementation of the MessageStore interface dealing in paired flat files.
Definition: gfilestore.h:56
static std::string x()
Returns the prefix for envelope header lines.
Definition: gfilestore.cpp:129
static Envelope readEnvelope(const G::Path &, std::ifstream *=nullptr)
Used by FileStore sibling classes to read an envelope file.
Definition: gfilestore.cpp:308
A somewhat opaque identifer for a GStore::MessageStore message id.
Definition: gmessagestore.h:43
std::string str() const
Returns the id string.
A concete class implementing the GStore::StoredMessage interface for separate envelope and content fi...
Definition: gstoredfile.h:52
A general-purpose exception class derived from std::exception and containing an error message.
Definition: gexception.h:64
A Path object represents a file system path.
Definition: gpath.h:82
std::string str() const
Returns the path string.
Definition: gpath.h:243
static std::string strerror(int errno_)
Translates an 'errno' value into a meaningful diagnostic string.
A class that calls an exit function at the end of its scope.
Definition: gscope.h:47
static void toLower(std::string &s)
Replaces all seven-bit upper-case characters in string 's' by lower-case characters.
Definition: gstr.cpp:819
static bool imatch(char, char) noexcept
Returns true if the two characters are the same, ignoring seven-bit case.
Definition: gstr.cpp:1415
static bool isNumeric(std::string_view s, bool allow_minus_sign=false) noexcept
Returns true if every character is a decimal digit.
Definition: gstr.cpp:400
static std::string tail(std::string_view in, std::size_t pos, std::string_view default_={})
Returns the last part of the string after the given position.
Definition: gstr.cpp:1322
A zero-copy string token iterator where the token separators are runs of whitespace characters,...
Definition: gstringtoken.h:54
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstringarray.h:30