# ================================================================== # Gossamer Threads Module Library - http://gossamer-threads.com/ # # GT::Mail::Editor # # Author: Jason Rhinelander # Revision: $Id: Editor.pm,v 1.25 2008/09/23 23:55:26 brewt Exp $ # # Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved. # ================================================================== # # The backend to a web-based e-mail template editor. See the pod for # instructions. This is designed the be used primarily from templates. # This module respects local directories on saving, and both local and # inheritance directories when loading. # # Also, any subclasses must be (something)::Editor # package GT::Mail::Editor; use strict; use vars qw($VERSION $DEBUG $ERRORS @ISA $ATTRIBS); use GT::Base; use GT::Template; @ISA = 'GT::Base'; $DEBUG = 0; $VERSION = sprintf "%d.%03d", q$Revision: 1.25 $ =~ /(\d+)\.(\d+)/; $ERRORS = { PARSE => "An error occurred while parsing: %s", NODIR => "Template directory not specified", BADDIR => "Template directory '%s' does not exist or has the permissions set incorrectly", NOFILE => "No template filename specified", CANT_CREATE_DIR => "Unable to create directory '%s': %s", BADFILE => "Template '%s' does not exist or is not readable", SAVEERROR => "Unable to open '%s' for writing: %s", LOADERROR => "Unable to open '%s' for reading: %s", RECURSION => "Recursive inheritance detected and interrupted: '%s'", INVALIDDIR => "Invalid template directory %s", INVALIDTPL => "Invalid template %s", }; $ATTRIBS = { dir => '', template => '', file => '', headers => undef, extra_headers => '', body => '' }; # GT::Mail::Editor::tpl_save(header => To => $header_to, header => From => $header_from, ..., extra_headers => $extra_headers) # ($extra_headers will be parsed). Everything is optional, but you should give something to build headers from. # It is not necessary to use To, From, etc. - you can enter them directly in the "extra_headers" field. sub tpl_save { # Have to extract the three-argument arguments BEFORE getting $self my @headers; for (my $i = 0; $i < @_; $i++) { if ($_[$i] eq 'header') { push @headers, (splice @_, $i, 3)[1,2]; redo; } } my $self = &_get_self; for (my $i = 0; $i < @headers; $i += 2) { $self->{headers}->{$headers[$i]} = $headers[$i+1]; } if ($self->{extra_headers}) { for (split /\s*\n\s*/, $self->{extra_headers}) { # This will weed out any blank lines my ($key, $value) = split /\s*:\s*/, $_, 2; $self->{headers}->{$key} = $value if $key and $value; } } my $dir; if ($self->{dir} and $self->{template}) { $dir = "$self->{dir}/$self->{template}/local"; if (!-d $dir) { # Attempt to create the "local" subdirectory mkdir($dir, 0777) or return $self->error(CANT_CREATE_DIR => 'FATAL' => $dir => "$!"); chmod(0777, $dir); } } elsif ($self->{dir}) { $dir = $self->{dir}; } local *FILE; $self->{_error} = []; if (not $dir) { $self->error(NODIR => 'WARN'); } elsif (not -d $dir or not -w $dir) { $self->error(BADDIR => WARN => $dir); } elsif (not $self->{file}) { $self->error(NOFILE => 'WARN'); } elsif (-f "$dir/$self->{file}" and not -w _) { $self->error(BADFILE => WARN => "$dir/$self->{file}"); } elsif (not open FILE, "> $dir/$self->{file}") { $self->error(SAVEERROR => WARN => "$dir/$self->{file}", "$!"); } else { # Everything is good, now we have FILE open to the file. $self->debug("Saving $dir/$self->{file}"); my $headers; while (my ($key, $val) = each %{$self->{headers}}) { next unless $key and $val; $key =~ s/\r?\n//g; $val =~ s/\r?\n//g; # Just in case... $headers .= "$key: $val\n"; } print FILE $headers; print FILE "" . "\n"; # Blank line $self->{body} =~ s/\r\n/\n/g; print FILE $self->{body}; close FILE; } if (@{$self->{_error}}) { return { error => join("
\n", @{$self->{_error}}) }; } else { return { success => 1, error => '' }; } } # GT::Mail::Editor::tpl_load(header => To, header => From, header => Subject) # In this case, "To", "From" and "Subject" will come to you as header_To, # header_From, and header_Subject. # What you get back is a hash reference, with either "error" set to an error # if something bad happened, or "success" set to 1, and the following template # variables: # # header_To, header_From, header_Subject, header_... # => The value of the To, From, Subject, etc. field. # -> Only present for individual headers that are requested with "header" # extra_headers => A loop of all the other headers with { name => To, From, etc., value => value } # body => The body of the e-mail. This will eventually change as this module # -> becomes capable of creating e-mails with multiple parts. sub tpl_load { my $self = &_get_self; my %sep_headers; for (my $i = 0; $i < @_; $i++) { if (lc $_[$i] eq 'header') { $sep_headers{$_[++$i]} = 1; } } my $dir; if ($self->{dir} and $self->{template} and $self->{file} and $self->{template} !~ m[[\\/\x00-\x1f]] and $self->{template} ne '..' and $self->{file} !~ m[[\\/\x00-\x1f]]) { $dir = "$self->{dir}/$self->{template}"; if (-f "$dir/local/$self->{file}") { $dir .= "/local"; } elsif (!-f "$dir/$self->{file}") { my ($tplinfo, %tplinfo); while ($tplinfo = GT::Template->load_tplinfo($dir) and my $inherit = $tplinfo->{inheritance}) { if ($inherit =~ m!^(?:[a-zA-Z]:)?[\\/]!) { # Absolute inheritance path $dir = $inherit; } else { $dir .= "/$inherit"; } if (-f "$dir/local/$self->{file}") { $dir .= "/local"; last; } elsif (-f "$dir/$self->{file}") { last; } if (length $dir > 150 or $tplinfo{$dir}++) { # We've already looked at that directory, or we just have too many relative paths tacked on the end $self->error(RECURSION => WARN => $dir); last; # End the loop - there is no more inheritance since we would just be recursing over what we already have } } } } my $fh = \do { local *FILE; *FILE }; $self->{_error} = []; my $return = { success => 0, error => '' }; if ($self->{template} =~ m[[\\/\x00-\x1f]] or $self->{template} eq '..') { $self->error(INVALIDDIR => WARN => $self->{template}); } elsif ($self->{file} =~ m[[\\/\x00-\x1f]]) { $self->error(INVALIDTPL => WARN => $self->{file}); } elsif (not $dir) { $self->error(NODIR => 'WARN'); } elsif (not -d $dir) { $self->error(BADDIR => WARN => $dir); } elsif (not $self->{file}) { $self->error(NOFILE => 'WARN'); } elsif (not -r "$dir/$self->{file}") { $self->error(BADFILE => WARN => "$dir/$self->{file}"); } elsif (not open $fh, "< $dir/$self->{file}") { $self->error(LOADERROR => WARN => "$dir/$self->{file}"); } else { # Everything is good, now we have $fh open to the file. $return->{success} = 1; $self->load($fh); while (my ($name, $val) = each %{$self->{headers}}) { if ($sep_headers{$name}) { $return->{"header_$name"} = $val; } else { push @{$return->{extra_headers}}, { name => $name, value => $val }; } } $return->{body} = $self->{body}; } if ($self->{_error}) { $return->{error} = join "
\n", @{$self->{_error}}; } return $return; } sub tpl_delete { my $self = &_get_self; if ($self->{dir} and $self->{template} and $self->{file} and $self->{template} !~ m[[\\/\x00-\x1f]] and $self->{template} ne '..' and $self->{file} !~ m[[\\/\x00-\x1f]]) { my $tpl = "$self->{dir}/$self->{template}/local/$self->{file}"; if (-f $tpl and not unlink $tpl) { return { error => "Unable to remove $tpl: $!" }; } } return { success => 1, error => '' }; } # Loads a template from a filehandle or a file. # You must pass in a GLOB reference as a filehandle to be read from. # Otherwise, this method will attempt to open the file passed in and then read from it. # (the file opened will have directory and template prepended to it). sub load { my $self = shift; my $fh; my $file = shift; if (ref $file eq 'GLOB' or ref $file eq 'SCALAR' or ref $file eq 'LVALUE') { $fh = $file; } else { $fh = \do { local *FILE; *FILE }; my $dir; if ($self->{template}) { $dir = "$self->{dir}/$self->{template}"; if (-f "$dir/local/$file") { $dir .= "/local"; } elsif (!-f "$dir/$file") { my ($tplinfo, %tplinfo); while ($tplinfo = GT::Template->load_tplinfo($dir) and my $inherit = $tplinfo->{inheritance}) { if ($inherit =~ m!^(?:[a-zA-Z]:)?[\\/]!) { # Absolute inheritance path $dir = $inherit; } else { $dir .= "/$inherit"; } if (-f "$dir/local/$file") { $dir .= "/local"; last; } elsif (-f "$dir/$file") { last; } if (length $dir > 150 or $tplinfo{$dir}++) { # We've already looked at that directory, or we just have too many relative paths tacked on the end $self->error(RECURSION => WARN => $dir); last; # End the loop - there is no more inheritance since we would just be recursing over what we already have } } } } $file = "$dir/$file"; open $fh, "< $file" or return $self->error(BADFILE => WARN => $file); } if (ref $fh eq 'GLOB') { while (<$fh>) { # The header s/\r?\n$//; last if not $_; # An empty line is the end of the headers my ($field, $value) = split /:\s*/, $_, 2; $self->{headers}->{$field} = $value; } while (<$fh>) { # The body $self->{body} .= $_; } } else { (my $header, $self->{body}) = split /\r?\n\r?\n/, $$fh, 2; my @h = split /\r?\n/, $header; for (@h) { my ($field, $value) = split /:\s*/, $_, 2; $self->{headers}->{$field} = $value; } } return 1; } # Creates and returns a $self object. Looks at $_[0] to see if it is already # an editor object, and if so uses that. Otherwise it calls new() with @_. # Should be called as &_get_self; If called as a class method, the first # argument will be removed. So, instead of: 'my $self = shift;' you should # use: 'my $self = &_get_self;' sub _get_self { my $self; if (ref $_[0] and substr(ref $_[0], -8) eq '::Editor') { # This will allow any subclass as long as it is something::Editor $self = shift; } elsif (@_ and substr($_[0], -8) eq '::Editor') { # Class methods my $class = shift; $self = $class->new(@_); } else { $self = __PACKAGE__->new(@_); } return $self; } sub init { my $self = shift; $self->set(@_); tie %{$self->{headers}}, __PACKAGE__ . '::Ordered'; } package GT::Mail::Editor::Ordered; # Implements a hash that retains the order elements are inserted into it. sub TIEHASH { bless { o => [], h => {}, p => 0 }, $_[0] } sub STORE { my ($self, $key, $val) = @_; $self->DELETE($key) if exists $self->{h}->{$key}; $self->{h}->{$key} = $val; push @{$self->{o}}, $key; } sub FETCH { $_[0]->{h}->{$_[1]} } sub FIRSTKEY { my $self = shift; $self->{p} = 0; $self->{o}->[$self->{p}++] } sub NEXTKEY { $_[0]->{o}->[$_[0]->{p}++] } sub EXISTS { exists $_[0]->{h}->{$_[1]} } sub DELETE { my ($self, $key) = @_; for (0 .. $#{$self->{o}}) { if ($self->{o}->[$_] eq $key) { splice @{$self->{o}}, $_, 1; last; } } delete $self->{h}->{$key}; } sub CLEAR { $_[0] = { o => [], h => {}, p => 0 }; () } 1; __END__ =head1 NAME GT::Mail::Editor - E-mail template editor =head1 SYNOPSIS Generally used from templates: <%GT::Mail::Editor::tpl_load( dir => $template_root, template => $template_set, file => $filename, header => From, header => To, header => Subject )%> <%if error%> Unable to load e-mail template: <%error%> <%else%> From: To: Subject: Other headers: