E-MailRelay
gmxfilter.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 gmxfilter.cpp
19///
20
21#include "gdef.h"
22#include "gmxfilter.h"
23#include "gstoredfile.h"
24#include "gprocess.h"
25#include "gaddress.h"
26#include "gnameservers.h"
27#include "gexception.h"
28#include "gscope.h"
29#include "gstr.h"
30#include "gdatetime.h"
31#include "gstringtoken.h"
32#include "glog.h"
33
35 Filter::Type filter_type , const Filter::Config & filter_config , const std::string & spec ) :
36 m_es(es) ,
37 m_store(store) ,
38 m_filter_type(filter_type) ,
39 m_filter_config(filter_config) ,
40 m_spec(spec) ,
41 m_id("mx:") ,
42 m_timer(*this,&MxFilter::onTimeout,es)
43{
44 if( MxLookup::enabled() )
45 m_mxlookup_config = parseSpec( m_spec , m_mxlookup_nameservers ) ;
46 else
47 throw G::Exception( "mx: not enabled at build time" ) ;
48}
49
51{
52 if( m_lookup )
53 m_lookup->doneSignal().disconnect() ;
54}
55
56void GFilters::MxFilter::start( const GStore::MessageId & message_id )
57{
58 G::Path envelope_path = m_store.envelopePath( message_id , storestate() ) ;
59 GStore::Envelope envelope = GStore::FileStore::readEnvelope( envelope_path ) ;
60 auto forward_to = parseForwardTo( envelope.forward_to ) ;
61 if( !forward_to.address.empty() )
62 {
63 // already an IP address so no DNS lookup
64 G_LOG_MORE( "GFilters::MxFilter::start: " << prefix() << " copying forward-to to forward-to-address: " << forward_to.address ) ;
65 GStore::StoredFile msg( m_store , message_id , storestate() ) ;
66 msg.noUnlock() ;
67 msg.editEnvelope( [forward_to](GStore::Envelope &env_){env_.forward_to_address=forward_to.address;} ) ;
68 m_timer.startTimer( 0U ) ;
69 m_result = Result::ok ;
70 }
71 else if( forward_to.domain.empty() )
72 {
73 G_LOG_MORE( "GFilters::MxFilter::start: " << prefix() << " no forward-to domain" ) ;
74 m_timer.startTimer( 0U ) ;
75 m_result = Result::ok ;
76 }
77 else
78 {
79 G_LOG( "GFilters::MxFilter::start: " << prefix() << " looking up [" << forward_to.domain << "]" ) ;
80
81 if( m_lookup ) m_lookup->doneSignal().disconnect() ;
82 m_lookup = std::make_unique<MxLookup>( m_es , m_mxlookup_config , m_mxlookup_nameservers ) ;
83 m_lookup->doneSignal().connect( G::Slot::slot(*this,&MxFilter::lookupDone) ) ;
84
85 m_lookup->start( message_id , forward_to.domain , forward_to.port ) ;
86 if( m_filter_config.timeout )
87 m_timer.startTimer( m_filter_config.timeout ) ;
88 else
89 m_timer.cancelTimer() ;
90 }
91}
92
93std::string GFilters::MxFilter::id() const
94{
95 return m_id ;
96}
97
98bool GFilters::MxFilter::quiet() const
99{
100 return false ;
101}
102
103bool GFilters::MxFilter::special() const
104{
105 return m_special ;
106}
107
108GSmtp::Filter::Result GFilters::MxFilter::result() const
109{
110 return m_result ;
111}
112
113std::string GFilters::MxFilter::response() const
114{
115 return { m_result == Result::fail ? "failed" : "" } ;
116}
117
118int GFilters::MxFilter::responseCode() const
119{
120 return 0 ;
121}
122
123std::string GFilters::MxFilter::reason() const
124{
125 return response() ;
126}
127
128G::Slot::Signal<int> & GFilters::MxFilter::doneSignal() noexcept
129{
130 return m_done_signal ;
131}
132
133void GFilters::MxFilter::cancel()
134{
135 if( m_lookup )
136 m_lookup->cancel() ;
137}
138
139void GFilters::MxFilter::onTimeout()
140{
141 G_DEBUG( "GFilters::MxFilter::onTimeout: response=[" << response() << "] special=" << m_special ) ;
142 m_done_signal.emit( static_cast<int>(m_result) ) ;
143}
144
145void GFilters::MxFilter::lookupDone( GStore::MessageId message_id , std::string address , std::string error )
146{
147 G_ASSERT( address.empty() == !error.empty() ) ;
148
149 // allow a special IP address to mean no forward-to-address
150 if( G::Str::headMatch(address,"0.0.0.0:") && GNet::Address::validString(address) )
151 address.clear() ;
152
153 G_LOG( "GFilters::MxFilter::start: " << prefix() << ": [" << message_id.str() << "]: "
154 << "setting forward-to-address [" << address << "]"
155 << (error.empty()?"":" (") << error << (error.empty()?"":")") ) ;
156
157 // update the envelope forward-to-address
158 GStore::StoredFile msg( m_store , message_id , storestate() ) ;
159 msg.noUnlock() ;
160 msg.editEnvelope( [address](GStore::Envelope &env_){env_.forward_to_address=address;} ) ;
161
162 m_result = error.empty() ? Result::ok : Result::fail ;
163 m_timer.startTimer( 0U ) ;
164}
165
166GStore::FileStore::State GFilters::MxFilter::storestate() const
167{
168 return m_filter_type == GSmtp::Filter::Type::server ?
169 GStore::FileStore::State::New :
170 GStore::FileStore::State::Locked ;
171}
172
173GFilters::MxLookup::Config GFilters::MxFilter::parseSpec( std::string_view spec , std::vector<GNet::Address> & nameservers_out )
174{
175 MxLookup::Config config ;
176 for( G::StringTokenView t(spec,";",1U) ; t ; ++t )
177 {
178 std::string_view s = t() ;
179 if( s.find("nst=") == 0U && s.size() > 4U && G::Str::isUInt(s.substr(4U)) )
180 config.ns_timeout = G::TimeInterval( G::Str::toUInt(s.substr(4U)) ) ;
181 else if( s.find("rt=") == 0U && s.size() > 3U && G::Str::isUInt(s.substr(3U)) )
182 config.restart_timeout = G::TimeInterval( G::Str::toUInt(s.substr(3U)) ) ;
183 else if( GNet::Address::validString( s ) )
184 nameservers_out.push_back( GNet::Address::parse( s ) ) ;
185 else if( GNet::Address::validStrings( s , "53" ) )
186 nameservers_out.push_back( GNet::Address::parse( s , "53" ) ) ;
187 else
188 G_WARNING_ONCE( "GFilters::MxFilter::parseSpec: invalid mx filter configuration: ignoring [" << G::Str::printable(s) << "]" ) ;
189 }
190 return config ;
191}
192
193std::string GFilters::MxFilter::addressLiteral( std::string_view s , unsigned int port )
194{
195 // RFC-5321 4.1.3
196 if( s.size() > 2U && s[0] == '[' && s[s.size()-1U] == ']' )
197 {
198 if( port == 0U ) port = 25U ;
199 s = s.substr( 1U , s.size()-2U ) ;
200 bool ipv6 = G::Str::ifind( s , "ipv6:" ) == 0U ;
201 if( ipv6 ) s = s.substr( 5U ) ;
202 if( GNet::Address::validStrings( s , std::to_string(port) ) )
203 {
204 auto address = GNet::Address::parse( s , port ) ;
205 if( address.family() == ( ipv6 ? GNet::Address::Family::ipv6 : GNet::Address::Family::ipv4 ) )
206 return address.displayString() ;
207 }
208 }
209 return {} ;
210}
211
212GFilters::MxFilter::ParserResult GFilters::MxFilter::parseForwardTo( const std::string & forward_to )
213{
214 // normally expect just a domain name but allow a ":<port>" suffix
215 // and ignore any "<user>@" prefix -- also allow a square-bracketed
216 // IP address that skips the MX lookup
217 //
218 auto no_user = G::Str::tailView( forward_to , "@" , false ) ;
219 std::size_t pos = no_user.rfind( ':' ) ;
220 auto head = G::Str::headView( no_user , pos , no_user ) ;
221 auto tail = G::Str::tailView( no_user , pos , {} ) ;
222 bool with_port = !tail.empty() && G::Str::isNumeric( tail ) ;
223 auto domain = with_port ? head : no_user ;
224 auto port = with_port ? G::Str::toUInt(tail) : 0U ;
225 return { G::sv_to_string(domain) , port , addressLiteral(domain,port) } ;
226}
227
228std::string GFilters::MxFilter::prefix() const
229{
230 return G::sv_to_string(strtype(m_filter_type)).append(" [").append(id()).append(1U,']') ;
231}
232
A concrete GSmtp::Filter class for message routing: if the message's 'forward-to' envelope field is s...
Definition: gmxfilter.h:48
~MxFilter() override
Destructor.
Definition: gmxfilter.cpp:50
MxFilter(GNet::EventState es, GStore::FileStore &, Filter::Type, const Filter::Config &, const std::string &spec)
Constructor.
Definition: gmxfilter.cpp:34
static bool enabled()
Returns true if implemented.
Definition: gmxlookup.cpp:42
static bool validString(std::string_view display_string, std::string *reason=nullptr)
Returns true if the transport-address display string is valid.
Definition: gaddress.cpp:383
static bool validStrings(std::string_view ip, std::string_view port_string, std::string *reason=nullptr)
Returns true if the combined network-address string and port string is valid.
Definition: gaddress.cpp:398
static Address parse(std::string_view display_string)
Factory function for any address family.
Definition: gaddress.cpp:178
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
A concrete implementation of the MessageStore interface dealing in paired flat files.
Definition: gfilestore.h:56
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
static bool isUInt(std::string_view s) noexcept
Returns true if the string can be converted into an unsigned integer without throwing an exception.
Definition: gstr.cpp:446
static bool headMatch(std::string_view in, std::string_view head) noexcept
Returns true if the string has the given start (or head is empty).
Definition: gstr.cpp:1359
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 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 std::size_t ifind(std::string_view s, std::string_view key)
Returns the position of the key in 's' using a seven-bit case-insensitive search.
Definition: gstr.cpp:1433
static std::string_view tailView(std::string_view in, std::size_t pos, std::string_view default_={}) noexcept
Like tail() but returning a view into the input string.
Definition: gstr.cpp:1337
static unsigned int toUInt(std::string_view s)
Converts string 's' to an unsigned int.
Definition: gstr.cpp:648
static std::string_view headView(std::string_view in, std::size_t pos, std::string_view default_={}) noexcept
Like head() but returning a view into the input string.
Definition: gstr.cpp:1308
A zero-copy string token iterator where the token separators are runs of whitespace characters,...
Definition: gstringtoken.h:54
An interval between two G::SystemTime values or two G::TimerTime values.
Definition: gdatetime.h:305
Slot< Args... > slot(TSink &sink, void(TSink::*method)(Args...))
A factory function for Slot objects.
Definition: gslot.h:240
A configuration structure for GFilters::MxLookup.
Definition: gmxlookup.h:53