Pages related to the web (HTTP, HTML, CSS, etc.).

Node

Node is a server-side JavaScript engine (i.e. it executes JavaScript without using a browser). This means that JavaScript developers can now develop tools in their native language, so it's not a surprise that the Bootstrap folks use Grunt for their build system. I'm new to the whole Node ecosystem, so here are my notes on how it works.

Start off by installing npm, the Node package manager. On Gentoo, that's:

# USE=npm emerge -av net-libs/nodejs

[Configure npm][npm-config] to make "global" installs in your personal space:

# npm config set prefix ~/.local/

Install the Grunt command line interface for building Bootstrap:

$ npm install -g grunt-cli

That installs the libraries under ~/.local/lib/node_modules and drops symlinks to binaries in ~/.local/bin (which is already in my PATH thanks to my dotfiles).

Clone Boostrap and install it's dependencies:

$ git clone git://github.com/twbs/bootstrap.git
$ cd bootstrap
$ npm install

This looks in the local [package.json][] to extract a list of dependencies, and installs each of them under node_modules. Node likes to isolate its packages, so every dependency for a given package is installed underneath that package. This leads to some crazy nesting:

$ find node_modules/ -name graceful-fs
node_modules/grunt/node_modules/glob/node_modules/graceful-fs
node_modules/grunt/node_modules/rimraf/node_modules/graceful-fs
node_modules/grunt-contrib-clean/node_modules/rimraf/node_modules/graceful-fs
node_modules/grunt-contrib-qunit/node_modules/grunt-lib-phantomjs/node_modules/phantomjs/node_modules/rimraf/node_modules/graceful-fs
node_modules/grunt-contrib-watch/node_modules/gaze/node_modules/globule/node_modules/glob/node_modules/graceful-fs

Sometimes the redundancy is due to different version requirements, but sometimes the redundancy is just redundant :p. Let's look with npm ls.

$ npm ls graceful-fs
bootstrap@3.0.0 /home/wking/src/bootstrap
├─┬ grunt@0.4.1
│ ├─┬ glob@3.1.21
│ │ └── graceful-fs@1.2.3
│ └─┬ rimraf@2.0.3
│   └── graceful-fs@1.1.14
├─┬ grunt-contrib-clean@0.5.0
│ └─┬ rimraf@2.2.2
│   └── graceful-fs@2.0.1
├─┬ grunt-contrib-qunit@0.2.2
│ └─┬ grunt-lib-phantomjs@0.3.1
│   └─┬ phantomjs@1.9.2-1
│     └─┬ rimraf@2.0.3
│       └── graceful-fs@1.1.14
└─┬ grunt-contrib-watch@0.5.3
  └─┬ gaze@0.4.1
    └─┬ globule@0.1.0
      └─┬ glob@3.1.21
        └── graceful-fs@1.2.3

Regardless of on-disk duplication, Node caches modules so a given module only loads once. If it really bothers you, you can avoid some duplicates by installing duplicated packages higher up in the local tree:

$ rm -rf node_modules
$ npm install graceful-fs@1.1.14
$ npm install
$ npm ls graceful-fs
bootstrap@3.0.0 /home/wking/src/bootstrap
├── graceful-fs@1.1.14  extraneous
├─┬ grunt@0.4.1
│ └─┬ glob@3.1.21
│   └── graceful-fs@1.2.3 
├─┬ grunt-contrib-clean@0.5.0
│ └─┬ rimraf@2.2.2
│   └── graceful-fs@2.0.1 
└─┬ grunt-contrib-watch@0.5.3
  └─┬ gaze@0.4.1
    └─┬ globule@0.1.0
      └─┬ glob@3.1.21
        └── graceful-fs@1.2.3 

This is probably not worth the trouble.

Now that we have Grunt and the Bootstrap dependencies, we can build the distributed libraries:

$ ~/src/node_modules/.bin/grunt dist
Running "clean:dist" (clean) task
Cleaning dist...OK

Running "recess:bootstrap" (recess) task
File "dist/css/bootstrap.css" created.

Running "recess:min" (recess) task
File "dist/css/bootstrap.min.css" created.
Original: 121876 bytes.
Minified: 99741 bytes.

Running "recess:theme" (recess) task
File "dist/css/bootstrap-theme.css" created.

Running "recess:theme_min" (recess) task
File "dist/css/bootstrap-theme.min.css" created.
Original: 18956 bytes.
Minified: 17003 bytes.

Running "copy:fonts" (copy) task
Copied 4 files

Running "concat:bootstrap" (concat) task
File "dist/js/bootstrap.js" created.

Running "uglify:bootstrap" (uglify) task
File "dist/js/bootstrap.min.js" created.
Original: 58543 bytes.
Minified: 27811 bytes.

Done, without errors.

Wohoo!

Unfortunately, like all language-specific packing systems, npm has trouble installing packages that aren't written in its native language. This means you get things like:

$ ~/src/node_modules/.bin/grunt
…
Running "jekyll:docs" (jekyll) task
`jekyll build` was initiated.

Jekyll output:
Warning: Command failed: /bin/sh: jekyll: command not found
 Use --force to continue.

Aborted due to warnings.

Once everybody wises up and starts writing packages for Gentoo Prefix, we can stop worrying about installation and get back to work developing :p.

Posted
Nginx

I transitioned from Apache to Nginx a week or so ago, since words like “minimal” and “streamlined” are appealing to me ;). I was quite happy with Apache, but it's always nice to try something new. Anyhow here's a quick review my configuration.

On Gentoo, set the the modules you want to install by adding the following lines to your /etc/make.conf:

NGINX_MODULES_HTTP="access auth_basic autoindex charset fastcgi gzip gzip_static limit_req map proxy rewrite scgi ssi stub_status"
NGINX_MODULES_MAIL=""

Then install Nginx with:

# emerge -av nginx

Make any adjustments you like to /etc/nginx/mime.types. I added:

types {
    …
    application/x-python                  py;
    application/x-shell                   sh;
    …
}

Now it's time to setup /etc/nginx/nginx.conf. Poking about online will give you lots of examples. Here are things that were useful to me, in the order they appear in the http block of my nginx.conf.

Redirecting www.example.com to example.com

This keeps people who accidentally add a www. prefix to your URL from matching the wildcard virtual host block defined below.

server {
  # www.example.com -> example.com
  listen 80;
  listen 443 ssl;
  server_name www.example.com;
  ssl_certificate /etc/ssl/nginx/www.example.com.pem;
  ssl_certificate_key /etc/ssl/nginx/www.example.com-key.pem;
  rewrite  ^/(.*)$  $scheme://example.com/$1  permanent;
}

Gitweb (and general CGI approach)

gitweb server:

server {
  listen 80;
  server_name git.example.com;

  access_log /var/log/nginx/git.example.com.access_log main;
  error_log /var/log/nginx/git.example.com.error_log info;

  root /usr/share/gitweb/;

  index gitweb.cgi;

  location /gitweb.cgi {
    include fastcgi_params;
    fastcgi_pass  unix:/var/run/fcgiwrap.sock-1;
  }
}

Because Nginx lacks built-in CGI support, we need some tricks to get gitweb.cgi working. We use the fcgi module to pass the requests on to a FastCGI server which wraps gitweb.cgi. On Gentoo, I installed the following packages:

  • www-misc/fcgiwrap, a FastCGI server for wrapping CGI scripts
  • www-servers/spawn-fcgi, a FastCGI manager for spawning fcgiwrap.

Configure spawn-fcgi to launch fcgiwrap with:

# cp /etc/conf.d/spawn-fcgi /etc/conf.d/spawn-fcgi.fcgiwrap
# emacs /etc/conf.d/spawn-fcgi.fcgiwrap
# cat /etc/conf.d/spawn-fcgi.fcgiwrap
FCGI_SOCKET=/var/run/fcgiwrap.sock
FCGI_ADDRESS=
FCGI_PORT=
FCGI_PROGRAM=/usr/sbin/fcgiwrap
FCGI_USER=nginx
FCGI_GROUP=nginx
FCGI_EXTRA_OPTIONS="-M 0700"
ALLOWED_ENV="PATH HOME"
HOME=/
FCGI_CHILDREN=1
FCGI_CHROOT=
# cd /etc/init.d/
# ln -s spawn-fcgi spawn-fcgi.fcgiwrap

Start fcgiwrap with:

# /etc/init.d/spawn-fcgi.fcgiwrap start

Add it to the default runlevel with:

# rc-update add spawn-fcgi.fcgiwrap default

If you also want a virtual host serving Git over HTTP, you can add a virtual host like:

server {
  # http-git.example.com
  listen 80;
  server_name http-git.example.com;

  access_log /var/log/nginx/http-git.example.com.access_log main;
  error_log /var/log/nginx/http-git.example.com.error_log info;

  location / {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /usr/libexec/git-core/git-http-backend;
    fastcgi_param GIT_HTTP_EXPORT_ALL "";
    fastcgi_param GIT_PROJECT_ROOT /var/git;
    fastcgi_param PATH_INFO $document_uri;
    fastcgi_pass  unix:/var/run/fcgiwrap.sock-1;
  }
}

This uses the same FastCGI server we used for gitweb, but this time the backend CGI script is git-http-backend.

Wildcard virtual hosts

To add support for virual hosts stored under /var/www/$host, use:

server {
  listen 80;
  #listen 443 ssl;

  server_name star.example.com *.example.com;

  access_log /var/log/nginx/star.example.com.access_log main;
  error_log /var/log/nginx/star.example.com.error_log info;

  #ssl_certificate /etc/ssl/nginx/$host.pem;
  #ssl_certificate_key /etc/ssl/nginx/$host.key;

  root /var/www/$host/htdocs;

  # deny access to .htaccess files
  location ~ /\.ht {
    deny all;
  }
}

Then adding a new host is as simple as creating a new entry in /var/www/ and updating your DNS to get the new name pointed at your server. Unfortunately, SSL/TLS doesn't work with this approach. It appears that certificates and keys are loaded when Nginx starts up, but $host is only defined after a request is received. Nginx does support SNI though, so it will work if you write SSL entries by hand for hosts that need them.

Main host

The configuration for my main host is more complicated, so I'll intersperse some more comments. I setup both clear-text and SSL in the same definition using the SSL module. The _ server name is a special name that matches any requests which haven't already matched and been handled by an earlier server.

server {
  # catchall virtual host (optional SSL, example.com)
  listen 80 default_server;
  listen 443 default_server ssl;
  server_name _;

  ssl_certificate /etc/ssl/nginx/example.com.pem;
  ssl_certificate_key /etc/ssl/nginx/example.com-key.pem;

Nothing special with the logging or root.

  access_log /var/log/nginx/example.com.access_log main;
  error_log /var/log/nginx/example.com.error_log info;

  root /var/www/example.com/htdocs;

Turn on SSI, and also use index.shtml as index pages.

  index index.html index.shtml;
  ssi on;

Use the proxy module to pass requests for /cookbook/ and subdirectories on to their underlying Django app.

  location /cookbook/ {
    proxy_pass  http://localhost:33333/cookbook/;
    proxy_set_header  X-Real-IP  $remote_addr;
  }

Use the scgi module to pass requests for /gallery/ and subdirectories on to their underlying SCGI app.

  location /gallery/ {
    include scgi_params;
    scgi_pass localhost:4000;
  }

Turn on autoindexing for /RAD/ and subdirectories using the autoindex module.

  location /RAD/ {
    autoindex on;
  }

Force SSL/TLS for /tree/ and subdirectories, redirecting plain-text requests to the equivalent HTTPS page. Use the auth_basic module for authentication, the SSL module for $ssl_protocol, and the rewrite module for the redirection.

  location /tree/ {
    auth_basic "Family Tree";
    auth_basic_user_file /home/jdoe/htpasswd;
    if ($ssl_protocol = "") {
      rewrite ^   https://example.com$request_uri? permanent;
    }
  }

Nothing special with the end of this server block.

  # deny access to .htaccess files
  location ~ /\.ht {
    deny all;
  }
}
Posted
Simple Common Gateway Interface

Basic CGI starts a new process to handle each request. This is simple, but resource hungry. To make things more efficient, a number of protocols have developed that keep a CGI server running in the background. The frontend webserver then passes requests back to the CGI server for processing without having to fire up a new program.

FastCGI is the original CGI server protocol, but here we'll focus on it's simpler cousin SCGI. I write most of my scripts in Python, so I use the handy scgi module to write the server (www-apps/scgi on Gentoo). There's a great article on Linux Journal to walk you through things, but in short, you need something like:

import scgi
import scgi.scgi_server
import urlparse

class YourHandler(scgi.scgi_server.SCGIHandler):
    def produce(self, env, bodysize, input, output):
        url = env.get('DOCUMENT_URI', None)
            if url is None:  # fall back to the request URI                         
            url = env.get('REQUEST_URI', None)
        data = urlparse.parse_qs(env.get('QUERY_STRING', ''))
        output.write('Status: 200 OK\r\n')
        output.write('Content-type: text/plain; charset=UTF-8\r\n')
        output.write('\r\n')
        output.write('URI: {}\n'.format(url))
        output.write('data: {}\n'.format(data))

if __name__ == '__main__':
    server = scgi.scgi_server.SCGIServer(
        handler_class=YourHandler, host='localhost', port=4000)
    server.serve()

Fire this script up, point your server at localhost:4000, and you're good to go!

I've written up a little script to test your SCGI server from the command line: scgi-test.py:

$ scgi-test.py /your/uri/
Status: 200 OK
Content-type: text/html; charset=UTF-8

<!DOCTYPE html>
…

Configuring your frontend webserver to point to the SCGI server is beyond the scope of this post. I give an Nginx example in my Nginx post.

Posted
twirssi

Twirssi is a Twitter/identi.ca plugin for irssi. Installation is pretty simple, but I've put a Gentoo package in my overlay in case you're picky about one-off installations or don't want to track down all those dependencies by hand.

Usage is also nice and simple. Here's an extract of the full docs:

  • Load the script: /script load twirssi.pl
  • Log in to twitter: /twitter_login <username>
  • Post: /tweet <status>
  • Follow: /twitter_follow [-w] <username> (with -w, send their tweets to a window of the same name)
  • Unfollow: /twitter_unfollow <username>
  • Search: /twitter_search <keyword>
  • Subscribe: /twitter_subscribe [-w] <keyword> (-w same as for /twitter_follow)
  • Unsubscribe: /twitter_unsubscribe <keyword>

Subscriptions are basically repeated searches. For example, if you wanted continuous updates on posts about twirssi, you would use:

/twitter_subscribe #twirssi

Twirssi saves your current settings (follows, subscriptions, window names, etc.) in ~/.irssi/scripts/twirssi.*.

You can direct a public tweet towards a user by including @<username> in your tweet text. You can also tag the tweet so that others can search for it with #keyword.

There is currently no way to list users that you are following (a.k.a. your friends) or your followers. Here's an excerpt from #twirssi on irc.foonetic.net.

--- Log opened Mon Feb 20 10:13:06 2012
10:13 < wking> is there a way to list users you are following (aka friends?) in twirssi?
10:15 <&Gedge> oh, just replied to your(?) tweet
10:16 < wking> ah, thanks.  Sorry for reposting ;)
10:17 <&Gedge> Not really (/twirssi_dump is debugging so doesn't count). Perhaps #twirssi needs "/twitter_friends" to list friends.
Posted
irssi

irssi is a great IRC client. The documentation is excellent, but long, so here is my summary of critical usage.

Connect to freenode as nickname:

$ irssi -c irc.freenode.net -n nickname

Get help:

irc> /help
irc> /help <command>

Join the #phys305 channel:

irc> /join phys305

Leave the current channel:

irc> /part

Quit your IRC session:

irc> /bye

Change to window <N> with Meta-<N>. The Esc key is one of a number of ways to change windows.

Talk by typing whatever you have to say. In a multi-user channel, it is good practice to preface responses with the nick of the person to whom you are responding. This makes it easier for them to pick relevant messages out during parallel conversations. irssi makes this easy by autocompleting nicks with Tab.

You can also look over the beginner section of the IRC mini-HOWTO. If you want to start a new channel, read The New IRC Channel Operator's Guide.

Posted
Local IRC server

Tonight I've been looking into ways to set up a local chat system for students in one of my courses. My initial thought was to use the NetKit talk at talkd programs, but I couldn't find an active homepage. Maybe they are so mature that noone needs to touch the code, but I expect they are actually just quietly dying in the corners of assorted harddrives, without any users to love them.

Since part of the goal of my course is to get the students up to speed on common open source development tools, my next thought was to go with a local IRC server. I know that there many wonderful public IRC servers (e.g. freenode), but why use a free service when you can run the service locally? Anyhow, here's my setup.

Local IRC server

There seem to be a number of these out there. I chose ngIRCd, because it's packaged for Gentoo (net-irc/ngircd), and it seems to be both cleanly coded and actively maintained (in Git ☺). Installation was a simple:

# emerge -av ngircd
# emacs /etc/ngircd/ngircd.conf
# cat /etc/ngircd/ngircd.conf
[Global]
    Name = irc.example.com
    AdminInfo1 = John Doe
    AdminInfo2 = 123 Street Rd.
    AdminEMail = jdoe@example.com
    Info = bla bla bla
    ServerGID = nogroup
    ServerUID = ngircd
[Options]
    PAM = no
# /etc/init.d/ngircd restart

I didn't add ngircd to the default runlevel. I'll just start it by hand whenever I want to use it.

Local connections

Using the excellent irssi client (net-irc/irssi on Gentoo):

$ irssi -c localhost

When you specify a server on the command line with -c server, irssi won't connect to any other servers for which you've configured automatic connection. In my case, this is what I want.

Exposing the IRC server on a remote host

Alright, I've got an IRC server running on my laptop, but I'm behind a firewall. I can expose my IRC server to the students by forwarding the IRC port to our department server, where the students all have shell accounts.

$ ssh -R localhost:6667:localhost:6667 physics.uni.edu

If someone else is already using port 6667 on the department server, it's easy to use another one:

$ ssh -R localhost:6668:localhost:6667 physics.uni.edu

Students can then connect by SSHing into the department server and running irssi there:

student@home $ ssh physics.uni.edu
student@physics $ irssi -c localhost -p 6668

And that's all there is too it! An easy way to introduce people to a popular tool.

Posted
HTML5 outlines

HTML5 has a well-defined outline algorithm (there's a nice overview at html5 doctor). Since browsers don't seem to support an outline view yet, I decided to embed an automatically-generated outline in a webpage using JavaScript. Thanks to the HTML5 outliner (h50) project, this is fairly straightforward. However, I couldn't find an example on their site, so here's a recipe to get you started.

Download the outliner javascript:

$ wget http://h5o.googlecode.com/files/outliner.0.5.0.62.js

or get the most recent version from the h5o download page. Then, start your page off with something like:

<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript" src="outliner.0.5.0.62.js"></script>
  <script type="text/javascript">
    var createLinks = true;
    var init=function()
    {
      var outline = HTML5Outline(document.body);
      var html = outline.asHTML(createLinks);
      document.getElementById('nav').innerHTML = html;
    }
  </script>
</head>
<body id="body" onload="init();">
<header>
<h1>PAGE TITLE</h1>
<nav id="nav">
  <script type="text/javascript">
    document.write('<h2 style="display: none">Table of Contents</h2>')
  </script>
</nav>
</header>

<h2 id="s1">FIRST SECTION</h2>

Important points:

  • The <nav> section is populated using JavaScript. If JavaScript is available, we get a nice table of contents. If JavaScript is not available, we go right from the page header to the first content section.
  • The <nav> title (Table of Contents) is hidden using CSS (display: none), since its role is fairly obvious. However, if you leave out the nav header entirely, the <nav> entry the table of contents will end up with an automatically generated title. For Firefox 8.0, that means Untitled NAV.
  • The <nav> title is inserted using document.write, so that browsers which support neither JavaScript nor CSS (e.g. w3m)—and therefore cannot generate the table of contents—will not display the Table of Contents header.
Posted
AWStats

AWStats is a log analyzer that gives you nice summaries about who's accessing your web server, when they're accessing it, how much bandwidth they're using, etc. Here's a quick run through installing and using AWStats on Gentoo.

Install AWStats:

# emerge -av awstats

Optionally, copy the log files from a remote server:

$ rsync -avz example.net:/var/log/apache2/ example-logs/

Configure AWStats by adjusting the model configuration distributed with AWStats.

$ cp /etc/awstats/awstats.model.conf awstats.example.conf
$ emacs awstats.example.conf

Here's an example config with the options I use:

# AWSTATS CONFIGURE FILE 7.0
Include "awstats.user.conf.common"
LogFile="example-logs/access_log"
LogType=W
LogFormat=1
LogSeparator=" "
SiteDomain="www.example.net"
HostAliases="localhost 127.0.0.1 REGEX[example\.net$]"
DNSLookup=1
DirData="data"
CreateDirDataIfNotExists=1
DirIcons="/usr/share/awstats/wwwroot/icon"
BuildHistoryFormat=text
LoadPlugin="decodeutfkeys"
LoadPlugin="ipv6"
LoadPlugin="hashfiles"

Because awstats.pl can be invoked interactively through your browser (depending on how you have it installed), it has a fairly restricted path. In order to run awstats on files in a random directory (e.g. for processing your server's logs on another host) you should use the -configdir option. Here's an example of parsing the all the archived logs (my logs are archived as access_log-YYYYMMDD.gz):

$ for log in example-logs/access_log-*.gz; do awstats.pl -configdir="${PWD}" -config example -LogFile="gzip -cd ${log} |"; done

I also analyze logs from another host, whose logs are archived weekly as access_log.2.gz, access_log.3.gz, etc. To parse the last year of those archived logs, use something like:

$ for i in $(seq 52 -1 2); do awstats.pl -configdir="${PWD}" -config=example -LogFile="gzip -cd example-logs/access_log.${i}.gz |"; done

After working through the archived logs, you can process the current log file:

$ awstats.pl -configdir="${PWD}" -config=example

Once all the logs have been processed, run

$ awstats.pl -configdir="${PWD}" -config=example -update

To generate the report, run

$ awstats_buildstaticpages.pl -configdir="${PWD}" -config=example -dir=html
Posted
Serving Git on Gentoo

Today I decided to host all my public Git repositories on my Gentoo server. Here's a quick summary of what I did.

Gitweb

Re-emerge git with the cgi USE flag enabled.

# echo "dev-util/git cgi" >> /etc/portage/package.use/webserver
# emerge -av git

Create a virtual host for running gitweb:

# cat > /etc/apache2/vhosts.d/20_git.example.net_vhost.conf << EOF
<VirtualHost *:80>
    ServerName git.example.net
    DocumentRoot /usr/share/gitweb
    <Directory /usr/share/gitweb>
        Allow from all
        AllowOverride all
        Order allow,deny
        Options ExecCGI
        <Files gitweb.cgi>
            SetHandler cgi-script
        </Files>
    </Directory>
    DirectoryIndex gitweb.cgi
    SetEnv  GITWEB_CONFIG  /etc/gitweb.conf
</VirtualHost>
EOF

Tell gitweb where you keep your repos:

# echo "\$projectroot = '/var/git';" > /etc/gitweb.conf

Tell gitweb where people can pull your repos from:

# echo "@git_base_url_list = ( 'git://example.net', ); >> /etc/gitweb.conf

Restart Apache:

# /etc/init.d/apache2 restart

Add the virtual host to your DNS server.

# emacs /etc/bind/pri/example.net.internal
...
git  A  192.168.0.2
...

Restart the DNS server.

# /etc/init.d/named restart

If names aren't showing up in the Owner column, you can add them to the user's /etc/passwd comment with

# usermod -c 'John Doe' jdoe

Thanks to Phil Sergi for his own summary, which I've borrowed from heavily.

Git daemon

Gitweb allows browsing repositories via HTTP, but if you will be pulling from your repositories using the git:// protocol, you'll also want to run git-daemon. On Gentoo, this is really easy, just edit /etc/conf.d/git-daemon as you see fit. I added --verbose, --base-path=/var/git and --export-all to GITDAEMON_OPTS. Start the daemon with

# /etc/init.d/git-daemon start

Add it to your default runlevel with

# rc-update add git-daemon default

If you're logging to syslog and running syslog-ng, you can configure the log location using the usual syslog tricks. See my syslog-ng for details.

Postfix

I spent some time today configuring Postfix so I could send mail from home via SMTPS.

Outgoing mail

Verizon, our ISP, blocks port 25 to external domains, forcing all outgoing mail through their outgoing.verizon.net exchange server (Comcast does the same thing). In order to accept mail, they also require you authenticate with your Verizon username and password, so I wanted to use an encrypted connection.

For the purpose of this example, our Verizon username is jdoe, our Verizon password is YOURPASS, you're running a local Postfix server on mail.example.com for your site at example.com, and 12345 is a free local port.

# cat /etc/postfix/main.cf
myhostname = mail.example.com
relayhost = [127.0.0.1]:12345
smtp_generic_maps = regexp:/etc/postfix/generic
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/saslpass
…
# cat /etc/postfix/generic
/^jdoe@(.*)$/ jdoe@example.com
/^root@(.*)$/ jdoe@example.com
# postmap /etc/postfix/generic
# cat /etc/postfix/saslpass
[127.0.0.1]:12345 jdoe@verizon.net:YOURPASS
# postmap /etc/postfix/saslpass
# cat /etc/stunnel/stunnel.conf
[smtp-tls-wrapper]
accept = 12345
client = yes
connect = outgoing.verizon.net:465
# /etc/init.d/stunnel restart
# postfix reload

Test with:

$ echo 'testing 1 2' | sendmail you@somewhere.com

Here's what's going on:

  • You hand an outgoing message to your local Postfix, which decides to send it via port 12345 on your localhost (127.0.0.1) (relayhost).
  • Stunnel picks up the connection from Postfix, encrypts everything, and forwards the connection to port 465 on outgoing.verizon.net (stunnel.conf).
  • Postfix identifies itself as mail.example.com (myhostname), and authenticates using your Verizon credentials (smtp_sasl_…).
  • Because Verizon is picky about the From addresses it will accept, we use smtp_generic_maps to map addresses to something simple that we've tested.

And that's it :p. If you're curious, there's more detail about all the Postfix config options in the postconf man page. You might also want to look over the SASL_README and ADDRESS_REWRITING_README.

For the generic mapping, I've used a regexp table, that way I don't have to map a bunch of possible original addresses by hand. By using smtp_generic_maps instead of canonical_maps, we only remap addresses before they head off into the wider world. If we used canonical_maps, we would remap all incoming mail, even mail destined for local delivery.

There's also a blog post by Tim White which I found useful. Because Verizon lacks STARTTLS support, his approach didn't work for me out of the box.

Incoming mail

In case you have trouble with someone blocking your incoming mail, things are a bit trickier. You can always accept mail on different ports (e.g. the submission port 587), with an entry like

submission inet n - n - - smtpd

in /etc/postfix/master.cf. However, others will not know which port you selected, because MX records do not allow you to specify alternate ports. The more modern SRV record allows this, but mail systems are old-school and don't support SRV. If you have access to another external server (whose port 25 isn't blocked), you can point your MX record at that server, and have it forward mail to you on your strange port.

For the purpose of this example, the remote host has a public IP of 1.2.3.4, and your local site is example.com, recieving mail on port 587. All of the following config files are on the remote host.

# cat /etc/postfix/main.cf
…
proxy_interfaces = 1.2.3.4
relay_domains = example.com
relay_transport = relay:[example.com]:587
…

For futher details (e.g. if you are relaying to more than one target), see the Postfix suggestions for being an MX host for a remote site.