Converting world timezones with DuckDuckGo and Wolfram Alpha from the browser address bar

I just can’t stop loving this.

By typing !wa time of some timezone in some other place in DuckDuckGo search box you get Wolfram Alpha results what the clock will look like some other place and in your local time. Example (with DuckDuckGo browser search integration, install from their homepage):

Screen Shot 2013-03-28 at 5.03.59 PM

This is the most excellent when agreeing on international meetings. Because if you do timezones in your head you will end up getting it wrong (summer time shifts, etc.) Also I have tried every other world time tool out there, starting from bought iPad and Mac App Store apps. I tried HTML5 apps. I tried Android apps. They all sucked.

Wolfram Alpha just rocks. And DuckDuckGo makes it accessible for me literally within one second, as I have my browser running all the time when I am working. If I ever meet a person who created this I’ll buy him a beer.

Also see my earlier DuckDuckGo power user tips.

\"\" Subscribe to RSS feed Follow me on Twitter Follow me on Facebook Follow me Google+

Building chat applications and robots for Skype

Command line is powerful interface. Everyone who has taken head first dive into UNIX and survived back to tell about it can confirm. However, you do not need to be a computer guru to interact with command line. In this blog post, I’ll want to open you the window of opportunity to realize some potential here in (Skype) chats. Please note that this blog post is not limited to the scope of Skype, but principles presented here can be applied to any chat media your team is using.

sevabot-256

1. Why build a chat robot?

Generally, non-IT-sawy folks interact with few command-line-likes in their daily work

  • Browser address bar (Firefox’s Awesome Bar has made awesome progress to shift my cognitive load into the browser address bar)
  • Search engine’s search box (autocompletion, but DuckDuckGo offers even more awesome tricks here)
  • Microsoft Excel input box, or similar
  • Chat and instant messaging message inputs
  • etc.

Command line is powerful, as stated before. It’s fast to type, but it’s also more error prone and more difficult to discover, as opposite to e.g. menu-driven interface or a web application. Clickety-click encourages discoverability by showing you the options as you progress. Command line is more like you need to know what you type and you do it as single pass – if there are errors you restart the process from the start. For this very reason command line is more effective for repeating tasks: typing has no speed limit and comes from your muscle memory. After you get past the command line learning curve it’ll be all harder, better, faster, stronger.

2. Skype chat, universal command line

Here is a real-life example: a managerial person in a small organization wanted to have a better picture what all of their team members are doing. Due to nature the of the work, the persons are not present at the office: they work here and there in client premises.

We could use a nice web app or even a mobile app to track the task everybody is working on. However, this particular manager thought “It’s too heavy weight.” The business case was that when he gets a call from a customer asking whether this task is done or started yet, he needs to know it at the moment. The organization is small, everybody is a geek and doing Bring Your Own Devices, so the app should be reachable on whatever you have in hand.

The manager had seen some the earlier automation efforts we had made with a Skype chat bot, Sevabot. So he asked me if we could build a Skype-chat-driven keep-it-simple task list application: just to mark whatever you are working on and mark again when you are done, all this by Skype chat commands.

Benefits of this approach

  • Everybody in the team was already using Skype: people are familiar with it
  • Skype is notoriously famous for working on every device, every network, everywhere
  • Skype syncs: even in offline you have some status messages left on your mobile device
  • Command line is efficient: Writing a chat message to output ongoing tasks can be done even when you are on mobile call. It literally takes a second.
  • Writing a simple command-driven application has very good cost-benefit ratio: writing a text based applications is a quick task, can be done in couple of hours (as opposite to build a web app, some forms, etc.)

And this is how it works:

Screen Shot 2013-03-28 at 1.47.40 PM

3. Creating a stateful chat app with Sevabot

Sevabot is an on-going open source Skype chat bot project. Before (version 1.2) it already had support for running UNIX scripts through chat commands and HTTP webhooks to send chat notifications from external services. The task in hand could have been created with little bit UNIX scripting magic. However, I had been in touch with Naoto Yokoyama who had a need for more advanced Skype chat commands besides simple triggers. Based on the discussion and Naoto’s original work we created a stateful chat scripting support for Sevabot.

  • You define your (stateful) chat application as a simple Python module and class
  • The modules are reloadable
  • Installing a new stateful script is simple as dropping the .py file into the correct folder
  • The script can install event handlers for all raw Skype4Py events, including handling of Skype calls
  • You can utilize background processes, timers, threads, native extensions and all the power of normal long-running Python UNIX apps

Below is the Python source for the task management application described above.

Some notes about the module

  • We persistent the script state to the disk. Sevabot is running in the cloud and it can be restarted at any moment. We simply use Python pickles, no databases needed here.
  • Each group chat has its own task list, so it is safe to multiplex one bot instance across different teams and working groups.
  • There is a timer which will poke the people if they forgot to close the tasks they have started.

4. Future

Next, I’d like to try the following

5. Source code

The module source code is also on Github and distributed with Sevabot 1.2.

#!/sevabot
"""

    Simple group chat task manager.

    This also serves as an example how to write stateful handlers.

"""

from __future__ import unicode_literals

from threading import Timer
from datetime import datetime
import os
import logging
import pickle
from collections import OrderedDict

from sevabot.bot.stateful import StatefulSkypeHandler
from sevabot.utils import ensure_unicode, get_chat_id

logger = logging.getLogger("Tasks")

# Set to debug only during dev
logger.setLevel(logging.INFO)

logger.debug("Tasks module level load import")

# How long one can work on a task before we give a warning
MAX_TASK_DURATION = 24*60*60

HELP_TEXT = """!tasks is a noteboard where virtual team members can share info which tasks they are currently working on.

Commands
------------------------------

!tasks: This help text

Start task: You start working on a task. When you started is recorded. Example:

    start task I am now working on new Sevabot module interface

Stop task: Stop working on the current task. Example:

    stop task

List tasks: List all tasks an people working on them. Example:

    list tasks

Task lists are chat specific and the list is secure to the members of the chat.
All commands are case-insensitive.
"""

class TasksHandler(StatefulSkypeHandler):
    """
    Skype message handler class for the task manager.
    """

    def __init__(self):
        """Use `init` method to initialize a handler.
        """
        logger.debug("Tasks constructed")

    def init(self, sevabot):
        """
        Set-up our state. This is called

        :param skype: Handle to Skype4Py instance
        """
        logger.debug("Tasks init")
        self.sevabot = sevabot
        self.status_file = os.path.join(os.path.dirname(__file__), "sevabot-tasks.tmp")
        self.status = Status.read(self.status_file)

        self.commands = {
            "!tasks": self.help,
            "start task": self.start_task,
            "list tasks": self.list_tasks,
            "stop task": self.stop_task,
        }

        self.reset_timeout_notifier()

    def handle_message(self, msg, status):
        """Override this method to customize a handler.
        """

        # Skype API may give different encodings
        # on different platforms
        body = ensure_unicode(msg.Body)

        logger.debug("Tasks handler got: %s" % body)

        # Parse the chat message to commanding part and arguments
        words = body.split(" ")
        lower = body.lower()

        if len(words) == 0:
            return False

        # Parse argument for two part command names
        if len(words) >= 2:
            desc = " ".join(words[2:])
        else:
            desc = None

        chat_id = get_chat_id(msg.Chat)

        # Check if we match any of our commands
        for name, cmd in self.commands.items():
            if lower.startswith(name):
                cmd(msg, status, desc, chat_id)
                return True

        return False

    def shutdown(self):
        """ Called when the module is reloaded.
        """
        logger.debug("Tasks shutdown")
        self.stop_timeout_notifier()

    def save(self):
        """
        Persistent our state.
        """
        Status.write(self.status_file, self.status)

    def reset_timeout_notifier(self):
        """
        Check every minute if there are overdue jobs
        """
        self.notifier = Timer(60.0, self.check_overdue_jobs)
        self.notifier.daemon = True  # Make sure CTRL+C works and does not leave timer blocking it
        self.notifier.start()

    def stop_timeout_notifier(self):
        """
        """
        self.notifier.cancel()

    def help(self, msg, status, desc, chat_id):
        """
        Print help text to chat.
        """

        # Make sure we don't trigger ourselves with the help text
        if not desc:
            msg.Chat.SendMessage(HELP_TEXT)

    def warn_overdue(self, chat_id, job):
        """
        Generate overdue warning.
        """
        self.sevabot.sendMessage(chat_id, "Task hanging: %s started working on %s, %s" % (job.real_name, job.desc, pretty_time_delta(job.started)))
        job.warned = True

    def check_overdue_jobs(self):
        """
        Timer callback to go through jobs which might be not going forward.
        """

        found = False

        logger.debug("Running overdue check")

        now = datetime.now()

        for chat_id, chat in self.status.chats.items():
            for job in chat.values():
                if (now - job.started).total_seconds() > MAX_TASK_DURATION and not job.warned:
                    found = True
                    self.warn_overdue(chat_id, job)

        if found:
            logger.debug("Found overdue jobs")
            self.save()
        else:
            logger.debug("Did not found overdue jobs")

        # http://www.youtube.com/watch?v=ZEQydmaPjF0
        self.reset_timeout_notifier()

    def start_task(self, msg, status, desc, chat_id):
        """
        Command handler.
        """

        if desc.strip() == "":
            msg.Chat.SendMessage("Please give task description also")
            return

        tasks = self.status.get_tasks(chat_id)
        existing_job = tasks.get(msg.Sender.Handle, None)
        if existing_job:
            msg.Chat.SendMessage("Stopped existing task %s" % existing_job.desc)

        job = Job(msg.Sender.FullName, datetime.now(), desc)
        tasks = self.status.get_tasks(chat_id)
        tasks[msg.Sender.Handle] = job
        self.save()
        msg.Chat.SendMessage("%s started working on %s." % (job.real_name, job.desc))

    def list_tasks(self, msg, status, desc, chat_id):
        """
        Command handler.
        """

        jobs = self.status.get_tasks(chat_id).values()

        if len(jobs) == 0:
            msg.Chat.SendMessage("No active tasks for anybody")

        for job in jobs:
            msg.Chat.SendMessage("%s started working on %s, %s" % (job.real_name, job.desc, pretty_time_delta(job.started)))

    def stop_task(self, msg, status, desc, chat_id):
        """
        Command handler.
        """
        tasks = self.status.get_tasks(chat_id)
        if msg.Sender.Handle in tasks:
            job = tasks[msg.Sender.Handle]
            del tasks[msg.Sender.Handle]
            msg.Chat.SendMessage("%s finished" % job.desc)
        else:
            msg.Chat.SendMessage("%s had no active task" % msg.Sender.FullName)

        self.save()

class Status:
    """
    Stored pickled state of the tasks.

    Use Python pickling serialization for making status info persistent.
    """

    def __init__(self):
        # Chat id -> OrderedDict() of jobs mappings
        self.chats = dict()

    @classmethod
    def read(cls, path):
        """
        Read status file.

        Return fresh status if file does not exist.
        """

        if not os.path.exists(path):
            # Status file do not exist, get default status
            return Status()

        f = open(path, "rb")

        try:
            return pickle.load(f)
        finally:
            f.close()

    @classmethod
    def write(cls, path, status):
        """
        Write status file
        """
        f = open(path, "wb")
        pickle.dump(status, f)
        f.close()

    def get_tasks(self, chat_id):
        """
        Get jobs of a particular chat.
        """
        if not chat_id in self.chats:
            # Skype username -> Task instance mappings
            self.chats[chat_id] = OrderedDict()

        return self.chats[chat_id]

class Job:
    """
    Tracks who is doing what
    """

    def __init__(self, real_name, started, desc):
        """
        :param started: datetime when the job was started
        """
        self.started = started
        self.desc = desc
        self.real_name = real_name
        # Have we given timeout warning for this job
        self.warned = False

# The following has been
# ripped off from https://github.com/imtapps/django-pretty-times/blob/master/pretty_times/pretty.py

_ = lambda x: x

def pretty_time_delta(time):

    now = datetime.now(time.tzinfo)

    if time > now:
        past = False
        diff = time - now
    else:
        past = True
        diff = now - time

    days = diff.days

    if days is 0:
        return get_small_increments(diff.seconds, past)
    else:
        return get_large_increments(days, past)

def get_small_increments(seconds, past):
    if seconds < 10:
        result = _('just now')
    elif seconds < 60:
        result = _pretty_format(seconds, 1, _('seconds'), past)
    elif seconds < 120:
        result = past and _('a minute ago') or _('in a minute')
    elif seconds < 3600:
        result = _pretty_format(seconds, 60, _('minutes'), past)
    elif seconds < 7200:
        result = past and _('an hour ago') or _('in an hour')
    else:
        result = _pretty_format(seconds, 3600, _('hours'), past)
    return result

def get_large_increments(days, past):
    if days == 1:
        result = past and _('yesterday') or _('tomorrow')
    elif days < 7:
        result = _pretty_format(days, 1, _('days'), past)
    elif days < 14:
        result = past and _('last week') or _('next week')
    elif days < 31:
        result = _pretty_format(days, 7, _('weeks'), past)
    elif days < 61:
        result = past and _('last month') or _('next month')
    elif days < 365:
        result = _pretty_format(days, 30, _('months'), past)
    elif days < 730:
        result = past and _('last year') or _('next year')
    else:
        result = _pretty_format(days, 365, _('years'), past)
    return result

def _pretty_format(diff_amount, units, text, past):
    pretty_time = (diff_amount + units / 2) / units

    if past:
        base = "%(amount)d %(quantity)s ago"
    else:
        base = "%(amount)d %(quantity)s"

    return base % dict(amount=pretty_time, quantity=text)

# Export the instance to Sevabot
sevabot_handler = TasksHandler()

__all__ = ["sevabot_handler"]

Enjoy.

 

 

 

\"\" Subscribe to RSS feed Follow me on Twitter Follow me on Facebook Follow me Google+

Using Postfix and free Mandrill email service for SMTP on Ubuntu Linux server

Update: This blog post is out-of-date. Mandrill recently changed their policy with changes that some of the service users may not approve. See alternatives.

OLD BLOG POST GOES HERE:

This blog posts discuss using Postfix mail server software and Mandrill SMTP service to send email out from your Linux server and web applications.

Getting a good outgoing email service for your self-hosted small business web application is difficult. Mandrill service addresses this issue; Mandrill is outgoing email service provided by Mailchimp email newsletter service – so they have all the money, skill and interest keep their SMTP server working and spam blacklist free.

In this blog post we discuss how to use Mandrill with Postfix SMTP daemon on Ubuntu Linux. Please note that this blog considers only Ubuntu Linux software stack where you have full control. If you run crippled deployment environment where you cannot install software, like Google AppEngine or Heroku, Mandrill offers HTTP API for sending email.

You may also want to see earlier blog posts Using Nullmailer and Mandrill app for sending email on Ubuntu servers and Sendmail using GMail and Nullmailer. However, approach described here is superior and recommended over the former alternatives. Specifically Exim4 and Sendmail are inhuman difficult to configure, MSMTP and Nullmailer do not offer localhost SMTP server.

1. About Mandrill

Screen Shot 2013-03-25 at 12.24.32 PM

Mandrill is

  • Real SMTP server with SMTP and HTTP APIs
  • 12k free emails per month – very generous from them
  • The owner has clear interest keeping it spam blacklist free (Mailchimp, their main product, is a newsletter service)
  • The web admin interface is easy and powerful
  • Mandrill supports converting all links in emails automatically trackable

Sidenote: when dealing with setting From: address in email make sure the domain records of the from email address whitelist the outgoing email server IP addresses via SPF records.

2. Setting up Postfix with Mandrill

The benefits of using Postfix include

  • Relative easy configuration
  • Local email queue on the server
  • SMTP service at localhost port 25. If you are hosting multiple web applications, like Plone CMS, they have configuration panel where to set SMTP server details. Postfix handles centralized SMTP upstream credential management and local queue for your outgoing emails. Postfix approach is more easier to migrate if you change SMTP servers: you change your ourgoing email service credentials just in one place
  • Some older web applications cannot do SMTP SSL/TLS authentication out of the box and thus cannot negotiate with Mandrill server (e.g. Plone 3.x, WordPress). Postfix acts as middleman  for them.

Screen Shot 2013-03-26 at 4.25.04 PM

Install Postfix on Ubuntu 12.04 LTS:

sudo apt-get install postfix mailutils libsasl2-2 ca-certificates libsasl2-modules

You may or may not popped up for Postfix setup wizard in this point. If it doesn’t happen manually run dpkg-reconfigure postfix after apt-get. When the setup wizard asks any questions choose

  • Internet site
  • Add fully qualified hostname for your server (e.g. if your host is foobar, FQDN is foobar.example.com)
  • Otherwise use the default settings

Screen Shot 2013-03-26 at 4.26.27 PM

Then add the following lines to file /etc/postfix/main.cf:

relayhost = [smtp.mandrillapp.com]
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_use_tls = yes

# Make sure Postfix listens to localhost only
inet_interfaces = 127.0.0.1

Set up your Mandrill credentials for Postfix:

echo "[smtp.mandrillapp.com]    MANDRILL_USERNAME:MANDRILL_API_KEY" > /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd

You can create your API key on Settings page on Mandrill website.

Install SSL certificates, so Postfix trusts smtp.mandrillapp.com (might not be supplied on Ubuntu server by default):

cat /etc/ssl/certs/Thawte_Premium_Server_CA.pem | sudo tee -a /etc/postfix/cacert.pem

Restart Postfix

service postfix reload && service postfix restart

3. Testing email out

Below is the command line to send some mail to yourself:

echo "This is a test message from ${USER}@${HOSTNAME} at $(date)" \
  | sendmail mikko@example.com

Check the system mail queue:

mailq  # Should be empty

You can see Postfix logs for possible detailed error reports:

tail -f /var/log/mail.log

See that Postfix answers in localhost port 25:

telnet localhost 25  # Write crap to the SMTP port until Postfix terminates the connection

Also check Outbound > Activity logs in Mandrill web interface.

4. More information

 

\"\" Subscribe to RSS feed Follow me on Twitter Follow me on Facebook Follow me Google+

Varnish at the front of WordPress + Apache and Plone CMS virtual hosts

When moving some sites to a new server I upgraded the Varnish cache server configuration serving this setup. Here are my notes how one can use Varnish at the front of virtual hosting.

The setup is following

  • Multiple sites are hosted on the same server. The sites are mix of PHP of Plone sites.
  • Varnish accepts HTTP requests in port 80 and forwards request to the corresponding backend through HTTP proxying
  • Our virtual host rules capture domain names with or without www-prefix, or with any subdomain name prefix
  • Apache runs in non-standard port localhost:81, serving PHP and WordPress. WordPress caching rules are defined in Apache <virtualhost> config file.
  • Every Plone site runs in its own port and process. Plone uses VirtualHostMonster to rewrite publicly facing site URLS. Plone caching HTTP headers are set by plone.app.caching addon.
  • We do extensive cookie sanitization for PHP (generic), WordPress and Plone.  Google Analytics etc. cookies don’t bust the cache and we can still login to WordPress and Plone as admin
  • As a special trick, there is cleaned cookie debugging trick through HTTP response headers

1-IMG_2677

Don’t worry, Varnish can handle little load

 

Pros

  • Blazingly fast, as Varnish is
  • With Plone’s plone.app.caching, one does not need to touch configuration files but Plone caching HTTP headers can be configured through-the-web

Cons

  • Varnish does not have Nginx or Apache style virtual host configuration file facilities by default and making includes is little bit tricky: With many virtualhost the default.vcl config file grows long.
  • Because WordPress cannot do static resource serving as smartly as Plone, which has unique URLs for all static media revisions, you need to purge Varnish manually from command line if you update any static media files like CSS, JS or images.

Varnish /etc/varnish/default.vcl example for Varnish 3.0:

#
# This backend never responds... we get hit in the case of bad virtualhost name
#
backend default {
    .host = "127.0.0.1";
    .port = "55555";
}

backend myplonesite {
    .host = "127.0.0.1";
    .port = "6699";
}

#
# Apache running on server port 81
#
backend apache {
    .host = "127.0.0.1";
    .port = "81";
}

#
# Gues which site / virtualhost we are diving into.
# Apache, Nginx or Plone directly
#
sub choose_backend {

    # WordPress site
    if (req.http.host ~ "^(.*\.)?opensourcehacker\.com(:[0-9]+)?$") {
        set req.backend = apache;
    }

    # Example Plone site
    if (req.http.host ~ "^(.*\.)?myplonesite\.fi(:[0-9]+)?$") {
        set req.backend = myplonesite;

        # Zope VirtualHostMonster
        set req.url = "/VirtualHostBase/http/" + req.http.host + ":80/Plone/VirtualHostRoot" + req.url;

    }

}

sub vcl_recv {

    #
    # Do Plone cookie sanitization, so cookies do not destroy cacheable anonymous pages.
    # Also, make sure we do not destroy WordPress admin and login cookies in the proces
    #
    if (req.http.Cookie && !(req.url ~ "wp-(login|admin)")) {
        set req.http.Cookie = ";" + req.http.Cookie;
        set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
        set req.http.Cookie = regsuball(req.http.Cookie, ";(statusmessages|__ac|_ZopeId|__cp|php|PHP|wordpress_(.*))=", "; \1=");
        set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");

        if (req.http.Cookie == "") {
            remove req.http.Cookie;
        }
    }

    call choose_backend;

    if (req.request != "GET" &&
      req.request != "HEAD" &&
      req.request != "PUT" &&
      req.request != "POST" &&
      req.request != "TRACE" &&
      req.request != "OPTIONS" &&
      req.request != "DELETE") {
        /* Non-RFC2616 or CONNECT which is weird. */
        return (pipe);
    }
    if (req.request != "GET" && req.request != "HEAD") {
        /* We only deal with GET and HEAD by default */
        return (pass);
    }
    if (req.http.Authorization || req.http.Cookie) {
        /* Not cacheable by default */
        return (pass);
    }
    return (lookup);
}

sub vcl_fetch {

    /* Use to see what cookies go through our filtering code to the server */
    /* set beresp.http.X-Varnish-Cookie-Debug = "Cleaned request cookie: " + req.http.Cookie; */

    if (beresp.ttl <= 0s ||
        beresp.http.Set-Cookie ||
        beresp.http.Vary == "*") {
        /*
         * Mark as "Hit-For-Pass" for the next 2 minutes
         */
        set beresp.ttl = 120 s;
        return (hit_for_pass);
    }
    return (deliver);
}

#
# Show custom helpful 500 page when the upstream does not respond
#
sub vcl_error {
  // Let's deliver a friendlier error page.
  // You can customize this as you wish.
  set obj.http.Content-Type = "text/html; charset=utf-8";
  synthetic {"
  <?xml version="1.0" encoding="utf-8"?>
  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  <html>
    <head>
      <title>"} + obj.status + " " + obj.response + {"</title>
      <style type="text/css">
      #page {width: 400px; padding: 10px; margin: 20px auto; border: 1px solid black; background-color: #FFF;}
      p {margin-left:20px;}
      body {background-color: #DDD; margin: auto;}
      </style>
    </head>
    <body>
    <div id="page">
    <h1>Sivu ei ole saatavissa</h1>
    <p>Pahoittelemme, mutta palvelua ei ole saatavilla.</p>
    <hr />
    <h4>Debug Info:</h4>
    <pre>Status: "} + obj.status + {"
Response: "} + obj.response + {"
XID: "} + req.xid + {"</pre>
      </div>
    </body>
   </html>
  "};
  return(deliver);
}

WordPress does not support setting HTTP response headers natively like Plone. We set them in Apache virtual host configuration file in /etc/apache2/sites-enabled:

<VirtualHost 127.0.0.1:81>

    ServerName opensourcehacker.com
    ServerAlias www.opensourcehacker.com
    ServerAdmin mikko@opensourcehacker.com

    LogFormat       combined
    TransferLog     /var/log/apache2/opensourcehacker.com.log

    # Basic WordPress setup

    Options +Indexes FollowSymLinks +ExecCGI

    DocumentRoot /srv/php/opensourcehacker/wordpress

    <Directory /srv/php/opensourcehacker/wordpress>
        Options FollowSymlinks
        AllowOverride All
    </Directory>

    AddType text/css .css
    AddType application/x-httpd-php .php .php3 .php4 .php5
    AddType application/x-httpd-php-source .phps

    #
    # Set expires headers manually
    #
    ExpiresActive On
    ExpiresByType text/html A0
    ExpiresByType image/gif A3600
    ExpiresByType image/png A3600
    ExpiresByType image/image/vnd.microsoft.icon A3600
    ExpiresByType image/jpeg A3600
    ExpiresByType text/css A3600
    ExpiresByType text/javascript A3600
    ExpiresByType application/x-javascript A3600

</VirtualHost>

\"\" Subscribe to RSS feed Follow me on Twitter Follow me on Facebook Follow me Google+