Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.idea
Rack*
start_mailphax_server.sh

5 changes: 3 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
source 'https://rubygems.org'
ruby '2.0.0'
ruby '2.3.0'

gem 'sinatra'
gem 'phaxio'
gem 'mail'
gem 'pony'
gem 'pony'
gem 'to_regexp'
7 changes: 6 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ GEM
httparty (0.13.0)
json (~> 1.8)
multi_xml (>= 0.5.2)
json (1.8.1)
json (1.8.2)
mail (2.6.1)
mime-types (>= 1.16, < 3)
mime-types (2.3)
Expand All @@ -25,6 +25,7 @@ GEM
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
tilt (1.4.1)
to_regexp (0.2.1)

PLATFORMS
ruby
Expand All @@ -34,3 +35,7 @@ DEPENDENCIES
phaxio
pony
sinatra
to_regexp

BUNDLED WITH
1.11.2
47 changes: 0 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,6 @@ Mailphax

Send faxes with Phaxio using 3rd party email services. Mailphax is a simple sinatra app. You can run it on any host or with any service that supports ruby and sinatra.


Installation on Heroku
------------

**Use the deploy button**

[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy)

**Or do it yourself**

(This assumes you have the Heroku toolbelt installed and have a Heroku account.)

1. git clone this repo && cd mailphax
1. heroku create
1. heroku config:set PHAXIO_KEY=yourPhaxioApiKey
1. heroku config:set PHAXIO_SECRET=yourPhaxioApiSecret
1. git push heroku master

Now set up your hosted email service to invoke callbacks to this service when mail is received. (See below.)

Configuring Mailgun
-------
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason this is being removed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Botch pull-request branch. This was intended for strictly a work-related project. Undone in #3.

1. Sign up for a mailgun account
Expand All @@ -36,30 +16,3 @@ Configuring Mailgun
1. For "Actions" specify "forward("http://yourMailPhaxInstallation/mailgun")" where yourMailPhaxInstallation should be the location where you've installed the sinatra app.
1. Click "Save".
1. Profit.


Configuring Mandrill
--------------------

1. Sign up for a mandrill account
1. In the Mandrill console, click "Inbound" in the left sidebar.
1. Add a new inbound domain that you have DNS control over.
1. Modify the DNS on your inbound domain to point to Mandrill using MX records. (Click the "DNS Settings" button for more info.)
1. Click "Routes" in the Mandrill console under your new inbound domain.
1. Add a wildcard route "*" and point it to http://yourMailPhaxInstallation/mandrill (e.g. http://example.com/mandrill)
1. Profit.


Configuring SendGrid
-------
TODO


TODOs
-----

- Support SendGrid
- Reply to user with confirmation of fax success/failure
- Reply to user if fax submission failed (e.g. bad number, no attachment)
- Allow filtering inbound emails by regexes

15 changes: 15 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@
"PHAXIO_SECRET": {
"description": "Your Phaxio API Secret"
},
"MAILGUN_KEY": {
"description": "Your Mailgun API Key"
},
"RECIPIENT_WHITELIST_FILE": {
"description": "File path to newline-separated list of whitelisted emails of recipients",
"required": false
},
"SENDER_WHITELIST_FILE": {
"description": "File path to newline-separated list of whitelisted emails of senders",
"required": false
},
"BODY_REGEX": {
"description": "Ruby Regexp string (of form '/{regex string}/{regex arguments}') to match against email body",
"required": false
},
"SMTP_HOST": {
"description": "SMTP host for outgoing email (for failure alerts)",
"required": false
Expand Down
174 changes: 141 additions & 33 deletions mailphax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,170 @@
require 'phaxio'
require 'mail'
require 'pony'
require 'tempfile'
require 'openssl'
require 'to_regexp'

if not ENV['PHAXIO_KEY'] or not ENV['PHAXIO_SECRET']
raise "You must specify your phaxio API keys in PHAXIO_KEY and PHAXIO_SECRET"
if not ENV['PHAXIO_KEY'] or not ENV['PHAXIO_SECRET'] or not ENV['MAILGUN_KEY']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mailgun is not necessarily required for all installations

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now that you addressed this in your comments and removed the other mail providers. you're probably right. we don't have the man power right now to implement the other providers unless they're requested.

raise "You must specify the required environment variables"
end

get '/' do
"Mailfax v1.0 - Visit a mail endpoint: (/sendgrid, /mandrill, /mailgun)"
"MailPhax v1.0 - Visit a mail endpoint: (/mailgun)"
end

get '/mailgun' do
[400, "Mailgun supported, but callbacks must be POSTs"]
end

get '/mandrill' do
[501, "mandrill not implemented yet"]
$recipientWhitelist = nil

def getRecipientWhitelist()
if $recipientWhitelist.nil?
Copy link
Contributor

@jnankin jnankin Feb 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should document these features in the readme (the new whitelists)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they are useful, and so I've added to the pull request, but I can also see it being too granular. If it is a worthwhile feature, I can add some documentation.

if ENV['RECIPIENT_WHITELIST_FILE']
$recipientWhitelist = File.read(ENV['RECIPIENT_WHITELIST_FILE']).split
end
end
return $recipientWhitelist
end

post '/mandrill' do
[501, "mandrill not implemented yet"]
$senderWhitelist = nil

def getSenderWhitelist()
if $senderWhitelist.nil?
if ENV['SENDER_WHITELIST_FILE']
$senderWhitelist = File.read(ENV['SENDER_WHITELIST_FILE']).split
end
end
return $senderWhitelist
end

get '/mailgun' do
[400, "Mailgun supported, but callbacks must be POSTs"]
$bodyRegex = nil

def getBodyRegex()
if $bodyRegex.nil?
if ENV['BODY_REGEX']
$bodyRegex = ENV['BODY_REGEX'].to_regexp
end
end
return $bodyRegex
end

def verifyMailgun(apiKey, token, timestamp, signature)
calculatedSignature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new(), apiKey, [timestamp, token].join())
signature == calculatedSignature
end

mailgunTokenCache = []

post '/mailgun' do
mailgunTokenCacheMaxLength = 50
timestampThreshold = 30.0

if not params['sender']
return [400, "Must include a sender"]
elsif not params['recipient']
return [400, "Must include a recipient"]
sender = params['sender']
if not sender
return logAndResponse(400, "Must include a sender", logger)
end

files = []
attachmentCount = params['attachment-count'].to_i
senderWhitelist = getSenderWhitelist()
if not senderWhitelist.nil? and not senderWhitelist.include? sender
return logAndResponse(401, "sender blocked", logger)
end

recipient = params['recipient']
if not recipient
return logAndResponse(400, "Must include a recipient", logger)
end

recipientWhitelist = getRecipientWhitelist()
if not recipientWhitelist.nil? and not recipientWhitelist.include? recipient
return logAndResponse(401, "recipient blocked", logger)
end

token = params['token']
if not token
return logAndResponse(400, "Must include a token", logger)
end

signature = params['signature']
if not signature
return logAndResponse(400, "Must include a signature", logger)
end

timestamp = params['timestamp']
if not timestamp
return logAndResponse(400, "Must include a timestamp", logger)
end

if mailgunTokenCache.include?(token)
return logAndResponse(400, "duplicate token", logger)
end

mailgunTokenCache.push(token)
while mailgunTokenCache.length() > mailgunTokenCacheMaxLength
mailgunTokenCache.pop()
end

timestampSeconds = timestamp.to_f
nowSeconds = Time.now().to_f
if (timestampSeconds - nowSeconds).abs() > timestampThreshold
return logAndResponse(400, "timestamp unsafe", logger)
end

if not verifyMailgun(ENV['MAILGUN_KEY'], token, timestamp, signature)
return logAndResponse(400, "signature does not verify", logger)
end

attachmentFiles = []

attachmentCount = params['attachment-count'].to_i
i = 1
while i <= attachmentCount do
#add the file to the hash
outputFile = "/tmp/#{Time.now.to_i}-#{rand(200)}-" + params["attachment-#{i}"][:filename]
tFile = Tempfile.new(['', params["attachment-#{i}"][:filename]])
data = params["attachment-#{i}"][:tempfile].read()
tFile.write(data)
tFile.close()

# use the whole file to ensure GC cannot release it yet
attachmentFiles.push(tFile)

File.open(outputFile, "w") do |f|
f.write(params["attachment-#{i}"][:tempfile].read)
i += 1
end

if params['body-plain']
data = params['body-plain']
bodyRegex = getBodyRegex()
if bodyRegex.nil? or bodyRegex.match(data)
tFile = Tempfile.new(['', 'email-body.txt'])
tFile.write(data)
tFile.close()

# use the whole file to ensure GC cannot release it yet
attachmentFiles.push(tFile)
else
return logAndResponse(401, "body not accepted", logger)
end
end

files.push(outputFile)
sendFax(sender, recipient, attachmentFiles)

i += 1
attachmentFiles.each do |attachmentFile|
begin
attachmentFile.unlink()
rescue
# do nothing
end
end

sendFax(params['sender'], params['recipient'],files)
"OK"
[200, "OK"]
end

get '/sendgrid' do
[501, "sendgrid not implemented yet"]
def logAndResponse(responseCode, message, logger)
logger.info(message)
return [responseCode, message]
end

def sendFax(fromEmail, toEmail, filenames)
def sendFax(fromEmail, toEmail, attachmentFiles)
Phaxio.config do |config|
config.api_key = ENV["PHAXIO_KEY"]
config.api_secret = ENV["PHAXIO_SECRET"]
Expand All @@ -67,18 +175,18 @@ def sendFax(fromEmail, toEmail, filenames)

options = {to: number, callback_url: "mailto:#{fromEmail}" }

filenames.each_index do |idx|
options["filename[#{idx}]"] = File.new(filenames[idx])
attachmentFiles.each_index do |idx|
options["filename[#{idx}]"] = File.new(attachmentFiles[idx].path)
end

logger.info "#{fromEmail} is attempting to send #{filenames.length} files to #{number}..."
logger.info("#{fromEmail} is attempting to send #{attachmentFiles.length} files to #{number}...")
result = Phaxio.send_fax(options)
result = JSON.parse result.body
result = JSON.parse(result.body)

if result['success']
logger.info "Fax queued up successfully: ID #" + result['data']['faxId'].to_s
logger.info("Fax queued up successfully: ID #" + result['data']['faxId'].to_s)
else
logger.warn "Problem submitting fax: " + result['message']
logger.warn("Problem submitting fax: " + result['message'])

if ENV['SMTP_HOST']
#send mail back to the user telling them there was a problem
Expand All @@ -87,7 +195,7 @@ def sendFax(fromEmail, toEmail, filenames)
:to => fromEmail,
:from => (ENV['SMTP_FROM'] || 'mailphax@example.com'),
:subject => 'Mailfax: There was a problem sending your fax',
:body => "There was a problem faxing your #{filenames.length} files to #{number}: " + result['message'],
:body => "There was a problem faxing your #{attachmentFiles.length} files to #{number}: " + result['message'],
:via => :smtp,
:via_options => {
:address => ENV['SMTP_HOST'],
Expand Down
16 changes: 16 additions & 0 deletions start_mailphax_server.sh.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

source /usr/local/rvm/scripts/rvm

PORT=8080
HOST='0.0.0.0'
ENVIRONMENT='production'

export PHAXIO_KEY='##PHAXIO API KEY##'
export PHAXIO_SECRET='##PHAXIO SECRET KEY##'
export MAILGUN_KEY='##MAILGUN API KEY##'

# see app.json for more available environment variables

cd /path/to/mailphax/git/repo/
rackup -p $PORT -o $HOST -E $ENVIRONMENT