├── .dockerignore ├── entrypoint.sh ├── Dockerfile ├── LICENSE ├── src └── server.js └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | node ./src/server.js | tee -a $1 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.4-alpine 2 | 3 | WORKDIR /usr/app 4 | COPY . /usr/app/ 5 | 6 | ENTRYPOINT ["./entrypoint.sh"] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Soheil Rashidi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const querystring = require('querystring'); 4 | 5 | const pixel = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvIAAAAAXRSTlMAQObYZgAAAApJREFUeNpjYQAAAAoABUouQOkAAAAASUVORK5CYII=', 'base64'); 6 | 7 | function log(type, request, query) { 8 | const log = { 9 | dateTime: new Date().toISOString(), 10 | type: type, 11 | ip: (request.headers && request.headers['x-forwarded-for']) || request.connection.remoteAddress || request.socket.remoteAddress || request.connection.socket.remoteAddress, 12 | headers: request.headers, 13 | data: query, 14 | }; 15 | 16 | console.log(JSON.stringify(log)); 17 | } 18 | 19 | function send(response, code, description, headers, body) { 20 | response.writeHead(code, description, headers); 21 | response.end(body); 22 | } 23 | 24 | const server = http.createServer((request, response) => { 25 | const request_url = url.parse(request.url); 26 | const query = querystring.parse(request_url.query); 27 | 28 | if (request.method !== 'GET') 29 | return send(response, 405, 'Method Not Allowed'); 30 | 31 | if (request_url.pathname === '/o') { 32 | log('open', request, query); 33 | 34 | return send(response, 200, 'OK', { 'Content-Type': 'image/png' }, pixel); 35 | } 36 | 37 | if (request_url.pathname === '/c') { 38 | if (!query.url) 39 | return send(response, 422, 'Unprocessable Entity'); 40 | 41 | log('click', request, query); 42 | 43 | return send(response, 302, 'Found', { 'Location': query.url }); 44 | } 45 | 46 | return send(response, 404, 'Not Found'); 47 | }); 48 | 49 | server.listen(process.argv[2] || 3000); 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # traq 2 | Super simple email open/click tracking server. 3 | 4 | For each event, it logs a JSON object to the output (a.k.a [JSON Lines](http://jsonlines.org/)). You can then pipe it to any program that you want to further processing. 5 | 6 | ``` 7 | { 8 | "dateTime": "2019-02-28T00:47:44.819Z", 9 | "type": "click", 10 | "ip": "93.184.216.34", 11 | "headers": { 12 | "host": "localhost:3000", 13 | "connection": "keep-alive", 14 | "upgrade-insecure-requests": "1", 15 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36", 16 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", 17 | "accept-encoding": "gzip, deflate, br", 18 | "accept-language": "en-US,en;q=0.9" 19 | }, 20 | "data": { 21 | "url": "http://example.com", 22 | "key1": "value1", 23 | "key2": "value2" 24 | } 25 | } 26 | ``` 27 | 28 | ## Usage 29 | 30 | Run the server: 31 | 32 | ``` 33 | node ./src/server.js [port] 34 | ``` 35 | 36 | Default port is 3000. 37 | 38 | For open tracking, insert an image into your email and point it to: 39 | 40 | ``` 41 | http://yourserver/o?key1=value1&key2=value2 42 | ``` 43 | 44 | For click tracking, change the links in your email to: 45 | 46 | ``` 47 | http://yourserver/c?url=&key1=value1&key2=value2 48 | ``` 49 | 50 | ## Docker 51 | 52 | The docker image writes the output to the file specified as the first argument: 53 | 54 | ``` 55 | docker run --rm -p 3000:3000 -v ./log:/var/log/traq soheilpro/traq /var/log/traq/traq.log 56 | ``` 57 | 58 | ## Version History 59 | + **1.0** 60 | + Initial release. 61 | 62 | ## Author 63 | **Soheil Rashidi** 64 | 65 | + http://soheilrashidi.com 66 | + http://twitter.com/soheilpro 67 | + http://github.com/soheilpro 68 | 69 | ## Copyright and License 70 | Copyright 2019 Soheil Rashidi. 71 | 72 | Licensed under the The MIT License (the "License"); 73 | you may not use this work except in compliance with the License. 74 | You may obtain a copy of the License in the LICENSE file, or at: 75 | 76 | http://www.opensource.org/licenses/mit-license.php 77 | 78 | Unless required by applicable law or agreed to in writing, software 79 | distributed under the License is distributed on an "AS IS" BASIS, 80 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 81 | See the License for the specific language governing permissions and 82 | limitations under the License. 83 | --------------------------------------------------------------------------------