├── .dockerignore ├── .gitignore ├── Dockerfile ├── Dockerfile.apk ├── Dockerfile.debian ├── Makefile ├── README.md ├── SConstruct ├── Vendor ├── catch │ └── catch.hpp └── json │ └── json.hpp ├── examples └── haproxy │ ├── .dockerignore │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ ├── haproxy.cfg │ └── services │ ├── .s6-svscan │ └── finish │ ├── haproxy │ ├── log │ │ └── run │ └── run │ ├── smtp-http-proxy │ ├── log │ │ └── run │ ├── notification-fd │ └── run │ └── syslogd │ ├── log │ └── run │ └── run ├── main.cpp ├── packages ├── alpine │ └── smtp-http-proxy │ │ └── APKBUILD └── debian │ └── smtp-http-proxy │ └── DEBIAN │ ├── .gitignore │ └── control ├── qa └── send_test_mails.sh └── unittests ├── .gitignore └── unittests.cpp /.dockerignore: -------------------------------------------------------------------------------- 1 | *.o 2 | smtp-http-proxy 3 | Makefile 4 | docker-build 5 | unittests/unittests 6 | .git/* 7 | packages/alpine/smtp-http-proxy/src 8 | .scon* 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /smtp-http-proxy 2 | *.o 3 | .scon* 4 | .DS_Store 5 | docker-build 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | 3 | RUN apk --no-cache add g++ scons curl-dev boost-dev bash 4 | 5 | ADD . /opt/src 6 | RUN cd /opt/src && scons && scons check=1 7 | RUN strip /opt/src/smtp-http-proxy 8 | 9 | CMD ["/bin/true"] 10 | -------------------------------------------------------------------------------- /Dockerfile.apk: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | 3 | RUN apk --no-cache add g++ scons curl-dev boost-dev bash 4 | RUN apk --no-cache add abuild 5 | RUN apk --no-cache add build-base 6 | ADD . /opt/src 7 | RUN \ 8 | echo 'PACKAGER_PRIVKEY="/opt/src/packages/alpine/build.rsa"' >> /etc/abuild.conf && \ 9 | adduser -S build && \ 10 | chown -R build /opt/src /var/cache/distfiles 11 | RUN \ 12 | sudo -u build /bin/sh -c 'cd /opt/src/packages/alpine/smtp-http-proxy && abuild' 13 | 14 | WORKDIR /opt/src 15 | CMD ["/bin/bash"] 16 | -------------------------------------------------------------------------------- /Dockerfile.debian: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | 3 | RUN apt-get update -y && apt-get -y install g++ scons libcurl4-openssl-dev libboost-dev build-essential 4 | RUN apt-get install -y libboost-system-dev libboost-program-options-dev libboost-log-dev 5 | RUN apt-get install -y pbuilder devscripts 6 | 7 | ADD . /opt/src 8 | RUN cd /opt/src && \ 9 | scons boost_libsuffix="" INSTALLDIR=packages/debian/smtp-http-proxy/usr && \ 10 | cd packages/debian && \ 11 | dpkg-deb --build smtp-http-proxy && \ 12 | mkdir -p /opt/packages/debian/smtp-http-proxy && \ 13 | mv smtp-http-proxy.deb /opt/packages/debian/smtp-http-proxy/smtp-http-proxy-0.5-1.deb && \ 14 | cd /opt/packages && \ 15 | dpkg-scanpackages debian/smtp-http-proxy /dev/null | gzip -9c > debian/smtp-http-proxy/Packages.gz 16 | 17 | CMD ["/bin/true"] 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_NAME=smtp-http-proxy 2 | APK_IMAGE_NAME=$(IMAGE_NAME)-apk 3 | DEBIAN_IMAGE_NAME=$(IMAGE_NAME)-debian 4 | 5 | .DEFAULT: build 6 | .PHONY: build 7 | build: 8 | docker build -t $(IMAGE_NAME) $(CURDIR) 9 | docker run --rm=true -v $(CURDIR)/docker-build:/opt/build $(IMAGE_NAME) cp /opt/src/smtp-http-proxy /opt/build 10 | 11 | .PHONY: apk 12 | apk: 13 | -rm -rf docker-build/apk 14 | mkdir -p docker-build/apk 15 | docker build -t $(APK_IMAGE_NAME) -f $(CURDIR)/Dockerfile.apk $(CURDIR) 16 | docker run --rm=true -v $(CURDIR)/docker-build:/opt/build $(APK_IMAGE_NAME) cp -r /home/build/packages /opt/build/apk 17 | 18 | .PHONY: apk-deploy 19 | apk-deploy: 20 | rsync -avz --delete docker-build/apk/packages/alpine/ el-tramo.be:cdn/alpine/smtp-http-proxy/ 21 | 22 | .PHONY: debian 23 | debian: 24 | -rm -rf docker-build/debian 25 | mkdir -p docker-build/debian 26 | docker build -t $(DEBIAN_IMAGE_NAME) -f $(CURDIR)/Dockerfile.debian $(CURDIR) 27 | docker run --rm=true -v $(CURDIR)/docker-build:/opt/build $(DEBIAN_IMAGE_NAME) sh -c 'cp -r /opt/packages/debian /opt/build' 28 | 29 | .PHONY: debian-deploy 30 | debian-deploy: 31 | rsync -avz --delete docker-build/debian/ el-tramo.be:cdn/debian/ 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [smtp-http-proxy: Tiny SMTP to HTTP bridge](https://el-tramo.be/smtp-http-proxy) 2 | 3 | `smtp-http-proxy` is a lightweight daemon that posts incoming SMTP requests 4 | to an HTTP URL as JSON. 5 | 6 | This is useful for services that use SMTP for things like reporting, 7 | such as [HAProxy](http://haproxy.org)'s `email-alert`. 8 | 9 | 10 | ## Installing 11 | 12 | ### Alpine 13 | 14 | echo http://cdn.el-tramo.be/alpine/smtp-http-proxy >> /etc/apk/repositories 15 | apk --allow-untrusted --no-cache add smtp-http-proxy 16 | 17 | ### Debian Stable (Jessie) 18 | 19 | echo 'deb http://cdn.el-tramo.be debian/smtp-http-proxy/' >> /etc/apt/sources.list 20 | apt-get update 21 | apt-get install smtp-http-proxy 22 | 23 | ## Building 24 | 25 | scons 26 | 27 | 28 | ## Usage 29 | 30 | The following will start listening for SMTP connections on port 25 of all interfaces, 31 | and send incoming messages to `https://example.com/receive-mail` 32 | 33 | smtp-http-proxy --bind 0.0.0.0 --port 25 --url https://example.com/receive-mail 34 | 35 | The given URL will get a HTTP `POST` request with an `application/json` body, 36 | of the following form: 37 | 38 | { 39 | "envelope": { 40 | "from": "", 41 | "to": [ 42 | "", 43 | "" 44 | ] 45 | }, 46 | "data": "From: sender@example.com\nDate: Sun, 12 Jun 2016 18:03:51 +0200\nSubject: Message\n\nThis is a message" 47 | } 48 | -------------------------------------------------------------------------------- /SConstruct: -------------------------------------------------------------------------------- 1 | import os, platform 2 | 3 | vars = Variables("config.py") 4 | vars.Add(BoolVariable("optimize", "Compile with optimizations turned on", "no")) 5 | vars.Add(BoolVariable("debug", "Compile with debug information", "yes")) 6 | vars.Add(BoolVariable("check", "Run unit tests", "no")) 7 | # FIXME: Don't hardcode this 8 | vars.Add(PathVariable("boost_includedir", "Boost headers location", "/usr/local/homebrew/opt/boost/include" , PathVariable.PathAccept)) 9 | vars.Add(PathVariable("boost_libdir", "Boost library location", "/usr/local/homebrew/opt/boost/lib", PathVariable.PathAccept)) 10 | vars.Add(PathVariable("boost_libsuffix", "Boost library suffix", "-mt", PathVariable.PathAccept)) 11 | 12 | env = Environment(ENV = {'PATH': os.environ['PATH']}, variables = vars) 13 | Help(vars.GenerateHelpText(env)) 14 | 15 | # Compiler 16 | if os.environ.get("CC", False) : 17 | env["CC"] = os.environ["CC"] 18 | if os.environ.get("CXX", False) : 19 | env["CXX"] = os.environ["CXX"] 20 | 21 | # Flags 22 | if env["PLATFORM"] == "win32" : 23 | env.Append(LINKFLAGS = ["/INCREMENTAL:no"]) 24 | env.Append(CCFLAGS = ["/EHsc", "/MD"]) 25 | env.Append(CPPDEFINES = [("_WIN32_WINNT", "0x0501")]) 26 | if env["debug"] : 27 | env.Append(CCFLAGS = ["/Zi"]) 28 | env.Append(LINKFLAGS = ["/DEBUG"]) 29 | # Workaround for broken SCons + MSVC2012 combo 30 | env["ENV"]["LIB"] = os.environ["LIB"] 31 | env["ENV"]["INCLUDE"] = os.environ["INCLUDE"] 32 | else : 33 | env["CXXFLAGS"] = ["-std=c++14"] 34 | if env["debug"] : 35 | env.Append(CCFLAGS = ["-g"]) 36 | env.Append(CXXFLAGS = ["-g"]) 37 | if env["optimize"] : 38 | env.Append(CCFLAGS = ["-O2"]) 39 | env.Append(CXXFLAGS = ["-O2"]) 40 | if env["PLATFORM"] == "posix" : 41 | env.Append(LIBS = ["pthread"]) 42 | env.Append(CXXFLAGS = ["-Wextra", "-Wall"]) 43 | elif env["PLATFORM"] == "darwin" : 44 | env["CC"] = "clang" 45 | env["CXX"] = "clang++" 46 | env["LINK"] = "clang++" 47 | if platform.machine() == "x86_64" : 48 | env["CCFLAGS"] = ["-arch", "x86_64"] 49 | env.Append(LINKFLAGS = ["-arch", "x86_64"]) 50 | env.Append(CXXFLAGS = ["-Wall", "-Wextra"]) 51 | 52 | if ARGUMENTS.get("INSTALLDIR", "") : 53 | if os.path.isabs(ARGUMENTS["INSTALLDIR"]) : 54 | env["INSTALLDIR"] = Dir(ARGUMENTS["INSTALLDIR"]).abspath 55 | else : 56 | env["INSTALLDIR"] = Dir("#/" + ARGUMENTS["INSTALLDIR"]).abspath 57 | 58 | # LibCURL 59 | libcurl_flags = { 60 | "LIBS" : ["curl"] 61 | } 62 | 63 | # Boost 64 | boost_flags = { 65 | "CXXFLAGS": ["-isystem", env["boost_includedir"]], 66 | "LIBPATH": [env["boost_libdir"]], 67 | "LIBS": ["boost_system${boost_libsuffix}", "boost_program_options${boost_libsuffix}", "boost_log_setup${boost_libsuffix}", "boost_log${boost_libsuffix}", "boost_thread${boost_libsuffix}"], 68 | "CPPDEFINES": ["BOOST_ALL_DYN_LINK"] 69 | } 70 | 71 | # Executable 72 | prog_env = env.Clone() 73 | prog_env.MergeFlags(libcurl_flags) 74 | prog_env.MergeFlags(boost_flags) 75 | prog_env.Append(CPPPATH = ["Vendor/json"]) 76 | prog = prog_env.Program("smtp-http-proxy", [ 77 | "main.cpp" 78 | ]) 79 | if prog_env.get("INSTALLDIR", "") : 80 | prog_env.Install(os.path.join(prog_env["INSTALLDIR"], "bin"), prog) 81 | 82 | # Tests 83 | check_env = env.Clone() 84 | check_env.Replace(CXXFLAGS = [f for f in env["CXXFLAGS"] if not f.startswith("-W")]) 85 | check_env.Append(CPPPATH = ["Vendor/catch"]) 86 | check_env.Append(CPPPATH = ["Vendor/json"]) 87 | unittests = check_env.Program("unittests/unittests", [ 88 | "unittests/unittests.cpp", 89 | ]) 90 | 91 | if env["check"] : 92 | check_env.Command("**dummy**", unittests, unittests[0].abspath) 93 | -------------------------------------------------------------------------------- /examples/haproxy/.dockerignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | -------------------------------------------------------------------------------- /examples/haproxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM haproxy:1.6-alpine 2 | 3 | # Install dependencies 4 | RUN apk --no-cache add s6 curl boost boost-program_options boost-system 5 | RUN \ 6 | curl http://cdn.el-tramo.be/alpine/el-tramo.be.rsa.pub > /etc/apk/keys/el-tramo.be.rsa.pub && \ 7 | echo http://cdn.el-tramo.be/alpine/smtp-http-proxy >> /etc/apk/repositories && \ 8 | apk --allow-untrusted --no-cache add smtp-http-proxy 9 | 10 | # Install configuration 11 | ADD services /etc/s6/services 12 | ADD haproxy.cfg /usr/local/etc/haproxy/ 13 | 14 | EXPOSE 80 15 | WORKDIR /etc/s6/services 16 | 17 | ENV SMTP_HTTP_URL=https://uu71rcz28i.execute-api.eu-central-1.amazonaws.com/prod/smtp2slack 18 | ENV AWS_LAMBDA_API_KEY=YOURKEY 19 | 20 | CMD ["s6-svscan"] 21 | -------------------------------------------------------------------------------- /examples/haproxy/Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_IP=$(shell echo $$DOCKER_HOST | sed 's/tcp:\/\///g' | sed 's/:.*//') 2 | 3 | .PHONY: docker 4 | docker: 5 | docker build -t haproxy-example . 6 | 7 | .PHONY: docker-run 8 | docker-run: 9 | @echo "Starting on http://$(DOCKER_IP):9080" 10 | docker run -it --rm=true -p 9080:80 -e AWS_LAMBDA_API_KEY=$(AWS_LAMBDA_API_KEY) --name haproxy-example haproxy-example $(DOCKER_COMMAND) 11 | 12 | -------------------------------------------------------------------------------- /examples/haproxy/README.md: -------------------------------------------------------------------------------- 1 | This directory contains a Docker image running a HAProxy instance that 2 | uses `smtp-http-proxy` to post to Slack. 3 | 4 | Full details can be found in 5 | [https://el-tramo.be/blog/haproxy-webhook-alerts/](this blog post). 6 | -------------------------------------------------------------------------------- /examples/haproxy/haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | maxconn 256 3 | log /dev/log local0 debug 4 | 5 | defaults 6 | mode http 7 | timeout connect 5000ms 8 | timeout client 50000ms 9 | timeout server 50000ms 10 | option http-server-close 11 | option httplog 12 | option dontlognull 13 | option dontlog-normal 14 | log global 15 | 16 | mailers alert-mailers 17 | mailer smtp1 127.0.0.1:8025 18 | 19 | frontend http-in 20 | bind *:80 21 | use_backend my-backend 22 | 23 | backend my-backend 24 | balance leastconn 25 | email-alert mailers alert-mailers 26 | email-alert from haproxy@el-tramo.be 27 | email-alert to haproxy-errors@el-tramo.be 28 | option httpchk GET /check 29 | server mysrv 1.2.3.4:80 check 30 | -------------------------------------------------------------------------------- /examples/haproxy/services/.s6-svscan/finish: -------------------------------------------------------------------------------- 1 | #!/bin/true 2 | -------------------------------------------------------------------------------- /examples/haproxy/services/haproxy/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec s6-log n20 s10485760 1 /var/log/haproxy-debug 4 | -------------------------------------------------------------------------------- /examples/haproxy/services/haproxy/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | s6-svwait -U /etc/s6/services/smtp-http-proxy 4 | s6-svwait /etc/s6/services/syslogd 5 | 6 | exec /usr/local/sbin/haproxy-systemd-wrapper -f /usr/local/etc/haproxy/haproxy.cfg -p /run/haproxy.pid 2>&1 7 | -------------------------------------------------------------------------------- /examples/haproxy/services/smtp-http-proxy/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec s6-log n20 s10485760 1 T /var/log/smtp-http-proxy 4 | -------------------------------------------------------------------------------- /examples/haproxy/services/smtp-http-proxy/notification-fd: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /examples/haproxy/services/smtp-http-proxy/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec smtp-http-proxy --debug -H "x-api-key: $AWS_LAMBDA_API_KEY" --url $SMTP_HTTP_URL --port 8025 --notify-fd 4 2>&1 4 | -------------------------------------------------------------------------------- /examples/haproxy/services/syslogd/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec s6-log n20 s10485760 1 /var/log/haproxy 4 | -------------------------------------------------------------------------------- /examples/haproxy/services/syslogd/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec syslogd -n -O /dev/stdout 4 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #define LOG(a) BOOST_LOG_TRIVIAL(a) 24 | 25 | using boost::asio::ip::tcp; 26 | using boost::asio::ip::address; 27 | namespace po = boost::program_options; 28 | 29 | class Sender { 30 | public: 31 | virtual void send(const std::string& response, bool close = false) = 0; 32 | }; 33 | 34 | class SMTPMessage { 35 | public: 36 | SMTPMessage( 37 | const std::string& from, 38 | const std::vector& to, 39 | const std::string& data) : 40 | from(from), 41 | to(to), 42 | data(data) { 43 | } 44 | 45 | const std::string& getFrom() const { 46 | return from; 47 | } 48 | 49 | const std::vector& getTo() const { 50 | return to; 51 | } 52 | 53 | const std::string& getData() const { 54 | return data; 55 | } 56 | 57 | private: 58 | std::string from; 59 | std::vector to; 60 | std::string data; 61 | }; 62 | 63 | class SMTPHandler { 64 | public: 65 | virtual void handle(const SMTPMessage& message) = 0; 66 | }; 67 | 68 | using json = nlohmann::json; 69 | 70 | static size_t curlWriteCallback(void* contents, size_t size, size_t nmemb, void*) { 71 | size_t realsize = size * nmemb; 72 | LOG(debug) << "HTTP: <- " << std::string((const char*) contents, realsize); 73 | return realsize; 74 | } 75 | 76 | static int curlDebugCallback(CURL*, curl_infotype type, char* data, size_t size, void *) { 77 | auto text = boost::algorithm::trim_right_copy(std::string(data, size)); 78 | switch (type) { 79 | case CURLINFO_TEXT: 80 | LOG(debug) << "HTTP: " << data; 81 | break; 82 | case CURLINFO_HEADER_OUT: 83 | LOG(debug) << "HTTP: -> H: " << text; 84 | break; 85 | case CURLINFO_DATA_OUT: 86 | break; 87 | case CURLINFO_SSL_DATA_OUT: 88 | break; 89 | case CURLINFO_HEADER_IN: 90 | LOG(debug) << "HTTP: <- H: " << text; 91 | break; 92 | case CURLINFO_DATA_IN: 93 | break; 94 | case CURLINFO_SSL_DATA_IN: 95 | break; 96 | default: 97 | return 0; 98 | } 99 | return 0; 100 | } 101 | 102 | class HTTPPoster : public SMTPHandler { 103 | public: 104 | HTTPPoster(const std::string& url, const std::vector& headers) : 105 | url(url), 106 | headers(headers), 107 | stopRequested(false) { 108 | thread = new std::thread(std::bind(&HTTPPoster::run, this)); 109 | } 110 | 111 | virtual void handle(const SMTPMessage& message) override { 112 | { 113 | std::lock_guard lock(queueMutex); 114 | queue.push_back(message); 115 | } 116 | queueNonEmpty.notify_one(); 117 | } 118 | 119 | void stop() { 120 | stopRequested = true; 121 | queueNonEmpty.notify_one(); 122 | thread->join(); 123 | delete thread; 124 | thread = 0; 125 | } 126 | 127 | private: 128 | void run() { 129 | while (!stopRequested) { 130 | boost::optional message; 131 | { 132 | std::unique_lock lock(queueMutex); 133 | queueNonEmpty.wait(lock, [this]() { return !queue.empty() || stopRequested; }); 134 | if (stopRequested) { break; } 135 | message = queue.front(); 136 | queue.pop_front(); 137 | } 138 | doHandle(*message); 139 | } 140 | } 141 | 142 | void doHandle(const SMTPMessage& message) { 143 | json j; 144 | j["envelope"] = { 145 | {"from", message.getFrom()}, 146 | {"to", message.getTo()} 147 | }; 148 | j["data"] = message.getData(); 149 | std::string body = j.dump(); 150 | 151 | LOG(info) << "Processing message: " << body; 152 | 153 | std::shared_ptr curl(curl_easy_init(), curl_easy_cleanup); 154 | struct curl_slist *slist = NULL; 155 | slist = curl_slist_append(slist, "Content-Type: application/json"); 156 | for (const auto& header : headers) { 157 | curl_slist_append(slist, header.c_str()); 158 | } 159 | curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, slist); 160 | curl_easy_setopt(curl.get(), CURLOPT_CUSTOMREQUEST, "POST"); 161 | curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, body.c_str()); 162 | curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, curlWriteCallback); 163 | curl_easy_setopt(curl.get(), CURLOPT_DEBUGFUNCTION, curlDebugCallback); 164 | curl_easy_setopt(curl.get(), CURLOPT_VERBOSE, 1); 165 | curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1); 166 | curl_easy_setopt(curl.get(), CURLOPT_MAXREDIRS, 5); 167 | curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); 168 | 169 | auto ret = curl_easy_perform(curl.get()); 170 | if (ret != CURLE_OK) { 171 | LOG(error) << "ERROR " << curl_easy_strerror(ret) << std::endl; 172 | } 173 | long statusCode; 174 | ret = curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &statusCode); 175 | if (ret != CURLE_OK) { 176 | LOG(error) << "ERROR " << curl_easy_strerror(ret) << std::endl; 177 | } 178 | if (statusCode != 200) { 179 | LOG(error) << "Error: Unexpected status code: " << statusCode; 180 | } 181 | } 182 | 183 | private: 184 | std::string url; 185 | std::vector headers; 186 | std::atomic_bool stopRequested; 187 | std::thread* thread; 188 | std::deque queue; 189 | std::mutex queueMutex; 190 | std::condition_variable queueNonEmpty; 191 | }; 192 | 193 | class SMTPSession { 194 | public: 195 | SMTPSession(Sender& sender, SMTPHandler& handler) : sender(sender), handler(handler), receivingData(false) { 196 | } 197 | 198 | void reset() { 199 | from = boost::optional(); 200 | to.clear(); 201 | dataLines.str(""); 202 | dataLines.clear(); 203 | } 204 | 205 | void start() { 206 | send("220 " + boost::asio::ip::host_name()); 207 | } 208 | 209 | void receive(const std::string& data) { 210 | LOG(debug) << "SMTP <- " << data; 211 | if (receivingData) { 212 | if (data == ".") { 213 | receivingData = false; 214 | send("250 Ok"); 215 | if (from) { 216 | handler.handle(SMTPMessage(*from, to, dataLines.str())); 217 | } 218 | else { 219 | LOG(warning) << "Didn't receive FROM; not handling mail"; 220 | } 221 | reset(); 222 | } 223 | else { 224 | dataLines << data << std::endl; 225 | } 226 | } 227 | else { 228 | if (boost::algorithm::starts_with(data, "HELO") || boost::algorithm::starts_with(data, "EHLO")) { 229 | send("250 Hello"); 230 | } 231 | else if (boost::algorithm::starts_with(data, "DATA")) { 232 | receivingData = true; 233 | send("354 Send data"); 234 | } 235 | else if (boost::algorithm::starts_with(data, "QUIT")) { 236 | send("221 Bye", true); 237 | } 238 | else if (boost::algorithm::starts_with(data, "MAIL FROM:")) { 239 | reset(); 240 | from = data.substr(10); 241 | send("250 Ok"); 242 | } 243 | else if (boost::algorithm::starts_with(data, "RCPT TO:")) { 244 | to.push_back(data.substr(8)); 245 | send("250 Ok"); 246 | } 247 | else if (boost::algorithm::starts_with(data, "NOOP")) { 248 | send("250 Ok"); 249 | } 250 | else if (boost::algorithm::starts_with(data, "RSET")) { 251 | reset(); 252 | send("250 Ok"); 253 | } 254 | else { 255 | send("502 Command not implemented"); 256 | } 257 | } 258 | } 259 | 260 | void send(const std::string& data, bool closeAfterNextWrite = false) { 261 | LOG(debug) << "SMTP -> " << data; 262 | sender.send(data, closeAfterNextWrite); 263 | } 264 | 265 | 266 | private: 267 | Sender& sender; 268 | SMTPHandler& handler; 269 | bool receivingData; 270 | boost::optional from; 271 | std::vector to; 272 | std::stringstream dataLines; 273 | }; 274 | 275 | template 276 | class LineBufferingReceiver { 277 | public: 278 | LineBufferingReceiver(T& target) : target(target) {} 279 | 280 | template 281 | void receive(InputIterator begin, InputIterator end) { 282 | while (begin != end) { 283 | char c = *begin++; 284 | if (c == '\n') { 285 | if (!buffer.empty()) { 286 | if (buffer.back() == '\r') { 287 | buffer.pop_back(); 288 | } 289 | target.receive(std::string(&buffer[0], buffer.size())); 290 | buffer.clear(); 291 | } 292 | } 293 | else { 294 | buffer.push_back(c); 295 | } 296 | } 297 | } 298 | 299 | private: 300 | T& target; 301 | std::vector buffer; 302 | }; 303 | 304 | class Session : public std::enable_shared_from_this, public Sender { 305 | public: 306 | Session(tcp::socket socket, HTTPPoster& httpPoster) : 307 | socket(std::move(socket)), 308 | smtpSession(*this, httpPoster), 309 | receiver(smtpSession) { 310 | } 311 | 312 | void start() { 313 | smtpSession.start(); 314 | } 315 | 316 | private: 317 | void doRead() { 318 | auto self(shared_from_this()); 319 | socket.async_read_some( 320 | boost::asio::buffer(buffer), 321 | [this, self](boost::system::error_code ec, std::size_t length) { 322 | if (!ec) { 323 | receiver.receive(buffer.data(), buffer.data() + length); 324 | doRead(); 325 | } 326 | }); 327 | } 328 | 329 | virtual void send(const std::string& command, bool closeAfterNextWrite) override { 330 | std::string cmd = command + "\r\n"; 331 | auto self(shared_from_this()); 332 | boost::asio::async_write( 333 | socket, boost::asio::buffer(cmd, cmd.size()), 334 | [this, self, closeAfterNextWrite](boost::system::error_code ec, std::size_t) { 335 | if (!ec) { 336 | if (closeAfterNextWrite) { 337 | boost::system::error_code errorCode; 338 | socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, errorCode); 339 | socket.close(); 340 | } 341 | else { 342 | doRead(); 343 | } 344 | } 345 | }); 346 | } 347 | 348 | tcp::socket socket; 349 | enum { maxLength = 8192 }; 350 | std::array buffer; 351 | SMTPSession smtpSession; 352 | LineBufferingReceiver receiver; 353 | }; 354 | 355 | class Server { 356 | public: 357 | Server( 358 | boost::asio::io_service& ioService, 359 | boost::asio::ip::address& bindAddress, 360 | int port, 361 | boost::optional notifyFD, 362 | HTTPPoster& httpPoster) : 363 | httpPoster(httpPoster), 364 | acceptor(ioService, tcp::endpoint(bindAddress, port)), 365 | socket(ioService) { 366 | doAccept(); 367 | if (notifyFD) { 368 | auto writeResult = write(*notifyFD, "\n", 1); 369 | if (writeResult <= 0) { LOG(error) << "Error " << writeResult << " writing to descriptor " << *notifyFD; } 370 | auto closeResult = close(*notifyFD); 371 | if (closeResult < 0) { LOG(error) << "Error " << closeResult << " closing descriptor " << *notifyFD; } 372 | } 373 | LOG(info) << "Listening for SMTP connections on " << tcp::endpoint(bindAddress, port); 374 | } 375 | 376 | private: 377 | void doAccept() { 378 | acceptor.async_accept(socket, [this](boost::system::error_code ec) { 379 | if (!ec) { 380 | std::make_shared(std::move(socket), httpPoster)->start(); 381 | } 382 | doAccept(); 383 | }); 384 | } 385 | 386 | HTTPPoster& httpPoster; 387 | tcp::acceptor acceptor; 388 | tcp::socket socket; 389 | }; 390 | 391 | int main(int argc, char* argv[]) { 392 | curl_global_init(CURL_GLOBAL_ALL); 393 | 394 | try { 395 | int port; 396 | std::string httpURL; 397 | std::vector httpHeaders; 398 | 399 | po::options_description options("Allowed options"); 400 | options.add_options() 401 | ("help", "Show this help message") 402 | ("verbose", "Enable verbose output") 403 | ("debug", "Enable debug output") 404 | ("notify-fd", po::value(), "Write to file descriptor when ready") 405 | ("bind", po::value()->default_value("0.0.0.0"), "SMTP address to bind") 406 | ("port", po::value(&port)->default_value(25), "SMTP port to bind") 407 | ("url", po::value(&httpURL)->required(), "HTTP URL") 408 | ("header,H", po::value>(&httpHeaders), "Extra HTTP Headers"); 409 | po::variables_map vm; 410 | po::store(po::parse_command_line(argc, argv, options), vm); 411 | if (vm.count("help")) { 412 | std::cout << options << "\n"; 413 | return 0; 414 | } 415 | po::notify(vm); 416 | 417 | boost::log::core::get()->set_filter(boost::log::trivial::severity >= boost::log::trivial::warning); 418 | boost::log::add_console_log(std::clog, boost::log::keywords::format = "[%TimeStamp%] %Message%"); 419 | boost::log::add_common_attributes(); 420 | 421 | boost::optional notifyFD; 422 | address bindAddress; 423 | 424 | if (vm.count("bind")) { 425 | bindAddress = address::from_string(vm["bind"].as()); 426 | } 427 | if (vm.count("verbose")) { 428 | boost::log::core::get()->set_filter(boost::log::trivial::severity >= boost::log::trivial::info); 429 | } 430 | if (vm.count("debug")) { 431 | boost::log::core::get()->set_filter(boost::log::trivial::severity >= boost::log::trivial::debug); 432 | } 433 | if (vm.count("notify-fd")) { 434 | notifyFD = vm["notify-fd"].as(); 435 | } 436 | 437 | boost::asio::io_service io_service; 438 | 439 | HTTPPoster httpPoster(httpURL, httpHeaders); 440 | Server s( 441 | io_service, 442 | bindAddress, 443 | port, 444 | notifyFD, 445 | httpPoster 446 | ); 447 | io_service.run(); 448 | 449 | httpPoster.stop(); 450 | 451 | } 452 | catch (boost::program_options::error& e) { 453 | std::cerr << e.what() << std::endl; 454 | } 455 | catch (std::exception& e) { 456 | LOG(fatal) << "Exception: " << e.what() << "\n"; 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /packages/alpine/smtp-http-proxy/APKBUILD: -------------------------------------------------------------------------------- 1 | # Contributor: Remko Tronçon 2 | # Maintainer: Remko Tronçon 3 | pkgname=smtp-http-proxy 4 | pkgver=0.5 5 | pkgrel=0 6 | pkgdesc="Simple SMTP to HTTP proxy" 7 | url="https://el-tramo.be/smtp-http-proxy" 8 | arch="all" 9 | license="BSD" 10 | depends="curl boost boost-program_options boost-system" 11 | makedepends="scons curl-dev boost-dev" 12 | install="" 13 | subpackages="" 14 | source="${pkgname}-${pkgver}.tar.gz::https://github.com/remko/smtp-http-proxy/archive/v$pkgver.tar.gz" 15 | 16 | _builddir="$srcdir"/smtp-http-proxy-$pkgver 17 | prepare() { 18 | local i 19 | cd "$_builddir" 20 | for i in $source; do 21 | case $i in 22 | *.patch) msg $i; patch -p1 -i "$srcdir"/$i || return 1;; 23 | esac 24 | done 25 | } 26 | 27 | build() { 28 | cd "$_builddir" 29 | scons || return 1 30 | } 31 | 32 | package() { 33 | cd "$_builddir" 34 | scons / || return 1 35 | mkdir -p "$pkgdir/usr/bin" 36 | cp smtp-http-proxy "$pkgdir/usr/bin" || return 1 37 | } 38 | md5sums="860dedb21525f7c6217e06bdb33bddad ${pkgname}-${pkgver}.tar.gz" 39 | sha256sums="fc23c729285d68a39bd42064cb8b993ce3e265d15450e2d4451da9e5a9c09a3b ${pkgname}-${pkgver}.tar.gz" 40 | sha512sums="0730a5856afd51752cc817c19ab8635e45feb1d3a423de0694220bcbec824457f2269263f568587a6251f11d68fbc2099f68bc363b9e7ade57496714ea138d6d ${pkgname}-${pkgver}.tar.gz" 41 | -------------------------------------------------------------------------------- /packages/debian/smtp-http-proxy/DEBIAN/.gitignore: -------------------------------------------------------------------------------- 1 | swift 2 | -------------------------------------------------------------------------------- /packages/debian/smtp-http-proxy/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: smtp-http-proxy 2 | Version: 0.5 3 | Section: custom 4 | Priority: optional 5 | Architecture: all 6 | Essential: no 7 | Depends: libboost-log1.55.0 (>= 1.55.0), libboost-program-options1.55.0 (>= 1.55.0), libboost-system1.55.0 (>= 1.55.0), libboost-thread1.55.0 (>= 1.55.0), libcurl3 (>= 7.38.0-4+deb8u3) 8 | Installed-Size: 1024 9 | Maintainer: Remko Tronçon 10 | Description: Lightweight SMTP to HTTP proxy 11 | -------------------------------------------------------------------------------- /qa/send_test_mails.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo -e 'Subject: Test 1\n\nThis is a test' | msmtp \ 4 | -d \ 5 | --host=127.0.0.1 --port=8025 \ 6 | --from=sender@example.com \ 7 | receiver@example.com 8 | echo -e 'Subject: Test 2\n\nThis is another test' | msmtp \ 9 | -d \ 10 | --host=127.0.0.1 --port=8025 \ 11 | --from=sender@example.com \ 12 | receiver@example.com 13 | -------------------------------------------------------------------------------- /unittests/.gitignore: -------------------------------------------------------------------------------- 1 | unittests 2 | -------------------------------------------------------------------------------- /unittests/unittests.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include "catch.hpp" 3 | --------------------------------------------------------------------------------