Wed, 03 Nov 2010

rel="nofollow" in Blosxom

I added rel="nofollow" support to the Blosxom writeback plugin. I use Doug Alcorn's version that supports a blacklist and emails me when a new comment/trackback is posted.

Only one small change was required. I simply modified the writeback plugin to prepend all href attributes with rel="nofollow".

--- writeback.orig      2005-01-20 10:59:23.000000000 -0800
+++ writeback   2005-01-20 11:31:00.000000000 -0800
@@ -233,6 +233,7 @@

         $writeback =~ s/\$writeback::(\w+)/$param{$1}/ge;
         $writeback =~ s!mailto:([^\s<]+)!<a href="mailto:$1">$1</a>!g;
+        $writeback =~ s!\bhref\b!rel="nofollow" href!ig;
         $writebacks .= $writeback;
         $count++;
       }

Updated: changed regex to match word boundaries around href.

tech | Permanent Link

Fri, 07 Nov 2008

Convert 15-character Salesforce IDs to 18-characters IDs with PHP

Saleforce uses 15-character IDs as primary keys for every record. When retrieving an ID over the API, 18-character case-insensitive IDs are returned. Generally, this doesn't cause any problems because the 18-character version can be used anywhere the 15-character one is. One problem arises though when you are storing IDs in a text field. In this case, you may need to convert a 15-character ID into its 18-character version. Salesforce provides a description of how to perform this conversion.

Here is a PHP implementation of the algorithm.

function sf15to18($short) {
   $chunks = str_split($short, 5);
   $extra = '';
   foreach ($chunks as $chunk) {
      $chars = str_split($chunk, 1);
      $bits = '';
      foreach ($chars as $char) {
         $bits .= (!is_numeric($char) && $char == strtoupper($char)) ? '1' : '0';
      }
      $map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345';
      $extra .= substr($map, base_convert(strrev($bits), 2, 10), 1);
   }
   return $short . $extra;
}

tech » salesforce | Permanent Link

Sun, 10 Feb 2008

Multi-Domain Tracking with Google Analytics/Urchin Tracking

If you are trying to track multiple domains within the same Google Analytics or Urchin profile, make sure you are using the same tracking library on all sites. I was attempting to track a visitor from one site using urchin.js to another using ga.js, and I spent a few hours trying to figure out why it wasn't working.

It turns out that the cookie data is encoded differently in query string appended to the url of the site to which you are passing it. With urchin.js and __utmLinker(), the __utmz cookie is url-encoded, but not with ga.js and ._link(). Compare the two URLs below.

http://example.com/?__utma=1.1181041260.1202679736.1202679736.1202679736.1&__utmb=1&__utmc=1.0&__utmx=-&__utmz=1.1202680382.1.3.utmcsr%3DFake%2BSource%7Cutmccn%3Dbogus%7Cutmcmd%3Dclicky%2Bclicky&__utmv=-&__utmk=129628685
http://example.com/?__utma=1.1181041260.1202679736.1202679736.1202679736.1&__utmb=1.0&__utmc=1.0&__utmx=-&__utmz=1.1202680287.1.2.utmcsr=Fake%2BSource|utmccn=bogus|utmcmd=clicky%2Bclicky&__utmv=-&__utmk=54196648

The ga.js _initData() won't properly parse the __utmz data from the query string in the first example. I hope this saves someone else some time.

tech | Permanent Link

Fri, 10 Aug 2007

Adding Outlook/Exchange Invitations to Google Calendar from mutt

If you work with people, you have probably received meeting requests from MS Outlook/Exchange users. I used to add these to KOrganizer using korganizerIn, but I stopped using KOrganizer a long time ago, and I've been pretty happy using Google Calendar for a while. My goal was to add meeting requests to my Google Calendar. The OpenSync library provides the means for moving events between different calendar formats, and multisync-tools provide a command-line interface to the OpenSync library.

Install and configure multisync-tools.

# apt-get install multisync-tools opensync-plugin-google-calendar opensync-plugin-file
$ mkdir ~/calendar
$ echo "text/calendar;~/bin/addCalendarItem %s; needsterminal;" >> ~/.mailcap
$ msynctool --addgroup file-google
$ msynctool --addmember file-google file-sync
$ msynctool --addmember file-google google-calendar
$ msynctool --configure file-google 1

Change the contents of the <path> tag.
<config><path>/home/dir/calendar</path><recursive>FALSE</recursive></config>
$ msynctool --configure file-google 2
Replace USER@gmail.com with your Google username and PASSWORD with your password.

Create ~/bin/addCalendarItem:

#!/bin/sh

echo loading $1;
cp $1 `tempfile -d ~/calendar`
msynctool --sync file-google 2> /dev/null

Now when you get a meeting request in mutt, you can view-attachments, then view-attach (<Return>) on the meeting.ics attachment, and the meeting will be added to your Google calendar. It's not perfect. No acceptance notifications are sent. If you view a meeting request multiple times, it will be added to the calendar multiple times. Sometimes the synchronization hangs, and when you try it again, it detects conflicts between what's in your calendar directory and your Google calendar, and you have to tell it which to keep.

If I ever get the motivation, I might write a script to just add events using the Google Calendar API since I don't really need two-way synchronization.

Thanks to Trond for getting me started.

tech | Permanent Link

Tue, 22 May 2007

Archiving Mail with Exim

I was recently asked to configure Exim to archive all mail sent and received by certain customers. Users authenticate to send mail using their email address so I used a domainlist to specify which domains' users should have their mail archived.

domainlist archive_domains = example.com

I created two routers and a transport for handling the mail sent by authenticated users. The first is a redirect router which rewrites the recipient address to a special address containing the sender's address, e.g. _!%#archive#%!_-user@example.com. This router has the unseen option set so the message is routed to the original recipient as usual. This doubles the number of recipients, but Exim discards duplicates so the final recipients are the original recipients plus the sender's archive copy. The second router strips the _!%#archive#%!_- prefix and delivers to the message to the sender's archive mailbox using a special transport.

These routers should probably be the first two since you don't want another router to accept delivery of the message first.

archive_by_sender_rewrite:
  driver = redirect
  condition = ${if and { {def:authenticated_id}{match_domain{${domain:$authenticated_id}}{+archive_domains}} }{yes}{no}}
  data = _!%#archive#%!_-$authenticated_id
  unseen
  no_repeat_use
  no_verify

archive_by_sender:
  local_part_prefix = _!%#archive#%!_-
  driver = accept
  no_verify
  transport=archive_by_sender

Because $authenticated_id is used to get the sender's address, you should have server_set_id = $1 in your authenticators so the variable gets set.

The router to archive recieved mail is pretty simple. It uses the unseen option again to create a copy of the message, and like archive_by_sender uses a separate transport to archive the message. This router should be placed before any routers that accept mail for the +archive_domains. If you use routers to discard or quarantine spam, this one should be before those if you want to archive the spam received.

archive_by_recipient:
  driver = accept
  domains = +archive_domains
  unseen
  no_verify
  transport=archive_by_recipient

Here are the transports. The messages are written to maildir directories. Any missing directories will be created if Exim has permission to create them.

archive_by_sender:
  driver = appendfile
  maildir_format
  mode = 0600
  mode_fail_narrower = false
  envelope_to_add = true
  return_path_add = true
  create_directory
  directory = /path/to/archive/$domain/$local_part/sent

archive_by_recipient:
  driver = appendfile
  maildir_format
  mode = 0600
  mode_fail_narrower = false
  envelope_to_add = true
  return_path_add = true
  create_directory
  directory = /path/to/archive/$domain/$local_part/received

If you don't have the default rule in your rcpt acl to reject local parts contains %, !, etc., you should make sure you don't accept mail for the special archive user address. Safeguarding against malicious users with shell access is left as an exercise for the reader. (Hint: I would probably look at $received_protocol.)

tech » mail | Permanent Link

Mon, 19 Mar 2007

Twitter

Mat Balez predicts the demise of Twitter. Twitter is kind of a micro-blogging tool. You can post messages to Twitter over the web, IM, and SMS. These messages are then sent to people in your social network, again through IM, SMS, or the web (html or rss). The messages are limited to 140 characters, and are meant to keep your friends up to date about the minutiae in your life.

Twitter has become quite popular, but Mat thinks it will fail pretty quickly because it offers no real value beyond the novelty and is too much work. I think the real reason Twitter is likely to fail is that it suffers from network effects that bend back on themselves. Like other networking sites like MySpace and LinkedIn, the service becomes more useful as more people use it. But with Twitter, beyond a certain level, it becomes too intrusive and therefore less useful. If you have a hundred friends posting every time they refill their coffee cup, you will be inundated with twitter spam (twam?). The novelty will turn annoyance.

tech | Permanent Link

Sat, 10 Mar 2007

Old Versions of Real Player

Real Player 10 has a nicer interface than version 8, but the video quality sucks. It's very choppy. So I recommend sticking with RealPlayer 8 for now. Real doesn't make it very easy to find old versions of their software, but there they do make them available in their Legacy Software Archive. For some reason, the link to RealPlayer 8 for Linux isn't the latest version. The Debian realplayer installer package expects rp8_linux20_libc6_i386_cs2_rpm. Also available is a tarball of the RealVideo 9 Codecs for use with RealPlayer 8. These are needed to play some videos.

tech | Permanent Link

Retrieving Old Messages on Blackberry

Looking at my web stats, I saw a few visitors from Google who were looking for information on how to get old messages onto a Blackberry. Since I didn't actually have that information, I'll add it here.

Suppose you have some old messages in your IMAP inbox which have been purged from your Blackberry because they are too old, and you would like to get them back so you can reply while on the subway. Just copy them from your inbox back to your inbox. You can do this in a single action with mutt--using save will copy the messages and delete the originals. But Thunderbird doesn't seem to allow you to create a copy of messages within a folder so you will have to move (copy and delete original) the messages to another folder, and then copy them back to your inbox.

The copies will then look like new messages to the Blackberry service, and your Blackberry will download them. If, within your non-mobile MUA, your sort your inbox by date or date-then-thread (mutt's threaded mode), your inbox should appear the same as before making the copies, but if you sort your messages in the order they exist in the IMAP store (including Thunderbird's threaded mode), the copies will appear at the end (or beginning, depending on your sorting direction) of your inbox.

tech » mail | Permanent Link

Use Separate State Directory for Burp

I recently bought a RAID enclosure for my backup drives. One of the nice features is that it powers down the drives and fans when there is no activity for five minutes.

I use burp for backing up my laptops. Burp has a timed backup mode, in which a client can connect to the server, and start a backup if it's been too long since its last backup. This allows, for example, wife to suspend her laptop whenever she wants. When it is running, burp will check with the server every 20 minutes whether it needs to be backed up. If she suspends it in the middle of a backup, it will resume during the next check.

I've configured burp to only allow backups outside of my normal working hours so I don't need to hear the drives grinding away while I'm at my desk. (Pulling myself away from my desk at night is the next challenge.)

The only problem with this configuration is that when a client connects, the backup directory is accessed to figure out whether to start a backup, causing the drives in my RAID enclosure to spin up. The solution was to not have burp touch the backup directory until it's starting a backup.

When a client connects, burp creates a lockfile. By default, this lockfile is created in the backup directory, but there is an option to use a different directory, client_lockdir. Set this in the server's burp.conf.

client_lockdir=/var/run/burp

The next change required is to have the timer script, which checks whether the server should start a backup when the client asks, use a separate directory to check the state of existing backups. First, enough data from the backup directory needs to be copied to a separate directory each time a backup is completed. To do this, I use the server_script_post option, which runs a script each time a backup completes.

server_script_post=/etc/burp/update_state_dir

My update_state_dir script copies the current symlinks and timestamp files from the backup directory to a separate state directory. I updated the timer script to check this state directory. Review all of the changes at github.

tech | Permanent Link

Wed, 28 Feb 2007

Power Magnet Update

In response to my questions about using Lua and Lighttpd to serve up static blosxom pages, Tomas Carnecky sent me an email with a better way to check for an empty query string within Lua, using "if next(get) then return 1 end". Here's my new power-magnet.cml

-- if query string is empty and static file exists, serve static file
dr = request["DOCUMENT_ROOT"]
-- local f=assert(io.open(dr.."/debug.out","a"))

if next(get) then return 1 end

sn = request["SCRIPT_NAME"]

static = string.gsub(sn, '^/blog/(.*)$', '/static/%1')

if (file_isdir(dr .. static) and file_isreg(dr .. static .. "/index.html")) then
--      f:write("cache hit on directory\n")
        output_include = { dr .. static .. "/index.html" }
        return 0
elseif file_isreg(dr .. static) then
--      f:write("cache hit on file\n")
        output_include = { dr .. static }
        return 0
end

-- f:write("no cache hit\n")
return 1

tech | Permanent Link

Newer Posts | Older Posts

The state is that great fiction by which everyone tries to live at the expense of everyone else. - Frederic Bastiat