package LMAP::CID2SPF;
=head1 NAME
LMAP::CID2SPF - Converts a MS Caller-ID entry (XML) to a SPF entry
=head1 SYNOPSIS
require LMAP::CID2SPF;
$c2s = LMAP::CID2SPF->new();
$c2s->cid("...");
$spf = $c2s->convert();
non-OO alternative:
require LMAP::CID2SPF qw(cid2spf);
$spf = cid2spf("...");
=head1 DESCRIPTION
This module can be used to convert between two of the LMAP (Lightweight MTA
Authentication Protocol) formats currently being proposed:
CID = Microsoft's Caller-ID
SPF = Sender Policy Framework
It will convert a XML-fragment as proposed by CID to a record that can be
added to DNS as proposed by SPF.
=head1 SEE ALSO
CID: L
SPF: L
=cut
# -------------------------------------------------------------------------
use strict;
use warnings;
use vars qw($VERSION);
$VERSION = "0.9";
use Carp;
# Export the cid2spf routine
use Exporter;
our @ISA = qw(Exporter);
our @EXPORT = qw();
our @EXPORT_OK = qw(cid2spf);
# We need this
require XML::Parser;
# -------------------------------------------------------------------------
=head1 PUBLIC INTERFACE
=cut
# -------------------------------------------------------------------------
=head2 OO calls
=over 4
=cut
# -------------------------------------------------------------------------
=item new [SOURCE]
I
Create a new converter object.
=cut
sub new {
my $self = shift;
my $type = ref($self) || $self;
my $me = bless {}, $type;
$me->{'cid'} = undef;
return $me;
}
# -------------------------------------------------------------------------
=item cid [cid-xml]
I
Sets or gets the CID-XML fragment for this converter object.
=cut
sub cid {
my $self = shift;
if (@_) {
$self->{'cid'} = shift;
}
return $self->{'cid'};
}
# -------------------------------------------------------------------------
=item convert
I
Converts the CID stored with cid() and returns the SPF-string.
=cut
sub convert {
my $self = shift;
if (! defined $self->{'cid'}) {
croak "CID is not set (call cid() first)";
}
$self->{'spf'} = [];
$self->{'has_error'} = 0;
# Parse the beast. Any XML-problem will be reported by XML::Parser.
LMAP::CID2SPF::XML::parse($self);
if ($self->{'has_error'}) {
croak $self->{'error'};
}
return $self->{'spf'};
}
# -------------------------------------------------------------------------
# NON-OO call
# -------------------------------------------------------------------------
=back
=head2 NON-OO call
=over 4
=cut
# -------------------------------------------------------------------------
=item cid2spf
Converts the given CID returning the corresponding SPF-string.
=cut
sub cid2spf {
my $cid = shift;
my $c = LMAP::CID2SPF->new();
$c->cid($cid);
return $c->convert();
}
# -------------------------------------------------------------------------
# XML::Parser callback subs:
# -------------------------------------------------------------------------
package LMAP::CID2SPF::XML;
our $me = undef;
our $action;
our $has_servers;
our $xmlp = undef;
# -------------------------------------------------------------------------
# Called with the LMAP::CID2SPF object as a parameter
sub parse {
my $obj = shift;
$action = '-all';
$has_servers = undef;
$me = $obj;
$xmlp = new XML::Parser(Style => 'Stream') if ! defined $xmlp;
$xmlp->parse($obj->{'cid'});
}
# -------------------------------------------------------------------------
sub StartTag {
my $hash = shift;
my $tag = shift;
my %attr = %_;
if ($tag eq 'm') {
if (defined $has_servers && ! $has_servers) {
$me->{'error'} = "Declared and later , this CID entry is not valid.\n";
$me->{'has_error'} = 1;
}
$has_servers = 1;
}
elsif ($tag eq 'noMailServers') {
if (defined $has_servers && $has_servers) {
$me->{'error'} = "Declared and later , this CID entry is not valid.\n";
$me->{'has_error'} = 1;
}
$has_servers = 0;
}
elsif ($tag eq 'ep') {
if (defined $attr{'testing'} && $attr{'testing'} eq 'true') {
# A CID with 'testing' found:
# From the MS-specs:
# "Documents in which such attribute is present with a true
# value SHOULD be entirely ignored (one should act as if the
# document were absent)"
# From the SPF-specs:
# "Neutral (?): The SPF client MUST proceed as if a domain did
# not publish SPF data."
# So we set SPF action to "neutral":
$action = '?all';
}
}
elsif ($tag eq 'mx') {
# The empty MX-tag, same as SPF's MX-mechanism
push @{ $me->{'spf'} }, 'mx';
}
}
# -------------------------------------------------------------------------
sub EndTag {
my $hash = shift;
my $tag = shift;
}
# -------------------------------------------------------------------------
sub EndDocument {
my $hash = shift;
# This is the end... print what we've got out
my $spf_entry = '';
$spf_entry .= 'v=spf1';
if ($has_servers) {
$spf_entry .= ' ' . join(' ', @{ $me->{'spf'} });
}
$spf_entry .= ' ' . $action;
# Store the result here
$me->{'spf'} = $spf_entry;
}
# -------------------------------------------------------------------------
sub Text {
my $hash = shift;
my $text = $hash->{'Text'};
my @context = @{ $hash->{'Context'} };
my $tag = $context[$#context];
# Remove starting and trailing spaces from text:
$text =~ s/^\s+//g;
$text =~ s/\s+$//g;
if ($tag eq 'a' || $tag eq 'r') {
# The A and R tags from MS-CID are both handled by the
# ipv4/6-mechanisms from SPF:
my $mechanism = 'ip4';
if ($text =~ /:/) {
$mechanism = 'ip6';
}
push @{ $me->{'spf'} }, $mechanism . ':' . $text;
}
if ($tag eq 'indirect') {
# MS-CID's indirect is "sort of" the include from SPF:
# Not really true, because the tag from MS-CID also
# provides a fallback in case the included domain doesn't provide
# _ep-records: The inbound MX-servers of the included domains
# are added to the list of allowed outgoing mailservers for the
# domain that declared the _ep-record with the tag.
# In SPF you would use the 'mx:domain' to handle this, but this
# wouldn't depend on referred domain having or not SPF-records.
push @{ $me->{'spf'} }, 'include:' . '_ep.' . $text;
}
}
1;
=back
=head1 AUTHOR
Copyright (C) 2004 Ernesto Baschny (F)
All rights reserved.
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=head1 VERSION
0.9
=cut