package WebGUI::Asset::Post; #------------------------------------------------------------------- # WebGUI is Copyright 2001-2009 Plain Black Corporation. #------------------------------------------------------------------- # Please read the legal notices (docs/legal.txt) and the license # (docs/license.txt) that came with this distribution before using # this software. #------------------------------------------------------------------- # http://www.plainblack.com info@plainblack.com #------------------------------------------------------------------- use strict; use Tie::CPHash; use Tie::IxHash; use WebGUI::Asset; use WebGUI::Asset::Template; use WebGUI::Asset::Post::Thread; use WebGUI::Cache; use WebGUI::Group; use WebGUI::HTML; use WebGUI::HTMLForm; use WebGUI::Form::DynamicField; use WebGUI::International; use WebGUI::Inbox; use WebGUI::Macro; use WebGUI::Mail::Send; use WebGUI::Operation; use WebGUI::Paginator; use WebGUI::SQL; use WebGUI::Storage; use WebGUI::User; use WebGUI::Utility; use WebGUI::VersionTag; our @ISA = qw(WebGUI::Asset); #------------------------------------------------------------------- =head2 _fixReplyCount ( asset ) Fixes the mismatch in number of replies and lastPost in a thread and/or a CS that occurs after a cut or paste action. Note: if invoked on a thread the CS containing it will very likely be changed as well, but likely in an incorrect manner. Therfore, after running this method on a Thread you probably also want to run it on the container CS. =head3 asset The instanciated asset to fix. This may only be either a WebGUI::Asset::Post::Thread or a WebGUI::Asset::Wobject::Collaboration. =cut sub _fixReplyCount { my $self = shift; my $asset = shift; my $lastPost = $asset->getLineage( [ qw{ self descendants } ], { returnObjects => 1, isa => 'WebGUI::Asset::Post', orderByClause => 'assetData.revisionDate desc', limit => 1, } )->[0]; if ($lastPost) { $asset->incrementReplies( $lastPost->get( 'revisionDate' ), $lastPost->getId ); } else { $asset->incrementReplies( undef, undef ); } } #------------------------------------------------------------------- =head2 addChild ( ) Overriding to limit the types of children allowed. =cut sub addChild { my $self = shift; my $properties = shift; my @other = @_; if ($properties->{className} ne "WebGUI::Asset::Post") { $self->session->errorHandler->security("add a ".$properties->{className}." to a ".$self->get("className")); return undef; } return $self->SUPER::addChild($properties, @other); } #------------------------------------------------------------------- =head2 addRevision ( ) Override the default method in order to deal with attachments. =cut sub addRevision { my $self = shift; my $newSelf = $self->SUPER::addRevision(@_); if ($newSelf->get("storageId") && $newSelf->get("storageId") eq $self->get('storageId')) { my $newStorage = WebGUI::Storage->get($self->session,$self->get("storageId"))->copy; $newSelf->update({storageId=>$newStorage->getId}); } my $threadId = $newSelf->get("threadId"); my $now = time(); if ($threadId eq "") { # new post if ($newSelf->getParent->isa("WebGUI::Asset::Wobject::Collaboration")) { $newSelf->update({threadId=>$newSelf->getId}); } else { $newSelf->update({threadId=>$newSelf->getParent->get("threadId")}); } delete $newSelf->{_thread}; } $newSelf->getThread->unmarkRead; return $newSelf; } #------------------------------------------------------------------- =head2 canAdd Extend the master class to make the default group 7. =cut sub canAdd { my $class = shift; my $session = shift; $class->SUPER::canAdd($session, undef, '7'); } #------------------------------------------------------------------- =head2 canEdit ($userId) If adding new posts, the check the parent's canPost method. If the user made this post, then check the editTimeout. Anyone in groupToEditPost is allowed to edit any post. Otherwise, anyone who canEdit the parent collaboration system can edit a post. =head3 $userId The userId of the user to check for permissions. If not passed, then it will use the session user instead. =cut sub canEdit { my $self = shift; my $userId = shift || $self->session->user->userId; my $session = $self->session; my $form = $self->session->form; my $user = WebGUI::User->new( $session, $userId ); # Handle adding new posts if ( ( $form->get("func") eq "add" || ( $form->get("func") eq "editSave" && $form->get("assetId") eq "new" ) ) && $form->get("class") eq "WebGUI::Asset::Post" ) { return $self->getThread->getParent->canPost; } # User who posted can edit their own post if ( $self->isPoster( $userId ) ) { my $editTimeout = $self->getThread->getParent->get( 'editTimeout' ); if ( $editTimeout > time - $self->get( "revisionDate" ) ) { return 1; } } # Users in groupToEditPost of the Collab can edit any post if ( $user->isInGroup( $self->getThread->getParent->get('groupToEditPost') ) ) { return 1; } return $self->getThread->getParent->canEdit( $userId ); } #------------------------------------------------------------------- =head2 canView ( ) Returns a boolean indicating whether the user can view the current post. =cut sub canView { my $self = shift; if (($self->get("status") eq "approved" || $self->get("status") eq "archived") && $self->getThread->getParent->canView) { return 1; } elsif ($self->canEdit) { return 1; } else { $self->getThread->getParent->canEdit; } } #------------------------------------------------------------------- =head2 chopTitle ( ) Cuts a title string off at 30 characters. =cut sub chopTitle { my $self = shift; return substr($self->get("title"),0,30); } #------------------------------------------------------------------- =head2 commit Extends the master class to notify subscribers, handle karmaPerPost, and increment replies for the parent thread. =cut sub commit { my $self = shift; $self->SUPER::commit; $self->notifySubscribers unless ($self->shouldSkipNotification); if ($self->isNew) { if ($self->session->setting->get("useKarma") && $self->getThread->getParent->get("karmaPerPost")) { my $u = WebGUI::User->new($self->session, $self->get("ownerUserId")); $u->karma($self->getThread->getParent->get("karmaPerPost"), $self->getId, "Collaboration post"); } $self->getThread->incrementReplies($self->get("revisionDate"),$self->getId);# if ($self->isReply); } } #------------------------------------------------------------------- =head2 cut Extend the master method to handle changing adjusting the number of replies to the parent thread. =cut sub cut { my $self = shift; # Fetch the Thread and CS before cutting the asset. my $thread = $self->getThread; my $cs = $thread->getParent; # Cut the asset my $result = $self->SUPER::cut; # If a post is being cut update the thread reply count first if ($thread->getId ne $self->getId) { $self->_fixReplyCount( $thread ); } # Update the CS reply count. This step is also necessary when a Post is cut since the Thread's incrementReplies # also calls the CS's incrementReplies, possibly with the wrong last post Id. $self->_fixReplyCount( $cs ); return $result; } #------------------------------------------------------------------- sub definition { my $class = shift; my $session = shift; my $definition = shift; my $i18n = WebGUI::International->new($session,"Asset_Post"); my $properties = { storageId => { fieldType=>"image", defaultValue=>'', enforceSizeLimits => 0, }, threadId => { noFormPost=>1, fieldType=>"hidden", defaultValue=>'', }, originalEmail => { noFormPost=>1, fieldType=>"hidden", defaultValue=>undef }, username => { fieldType=>"hidden", defaultValue=>$session->form->process("visitorUsername") || $session->user->profileField("alias") || $session->user->username }, rating => { noFormPost=>1, fieldType=>"hidden", defaultValue=>undef }, views => { noFormPost=>1, fieldType=>"hidden", defaultValue=>undef }, contentType => { fieldType=>"contentType", defaultValue=>"mixed" }, userDefined1 => { fieldType=>"HTMLArea", defaultValue=>undef }, userDefined2 => { fieldType=>"HTMLArea", defaultValue=>undef }, userDefined3 => { fieldType=>"HTMLArea", defaultValue=>undef }, userDefined4 => { fieldType=>"HTMLArea", defaultValue=>undef }, userDefined5 => { fieldType=>"HTMLArea", defaultValue=>undef }, content => { fieldType=>"HTMLArea", defaultValue=>undef }, }; push(@{$definition}, { assetName=>$i18n->get('assetName'), icon=>'post.gif', tableName=>'Post', className=>'WebGUI::Asset::Post', properties=>$properties, }); return $class->SUPER::definition($session,$definition); } #------------------------------------------------------------------- =head2 DESTROY Extend the base method to delete the locally cached thread object. =cut sub DESTROY { my $self = shift; $self->{_thread}->DESTROY if (exists $self->{_thread} && ref $self->{_thread} =~ /Thread/); $self->SUPER::DESTROY; } #------------------------------------------------------------------- =head2 exportAssetData ( ) Extend the base class to handle storage locations. =cut sub exportAssetData { my $self = shift; my $data = $self->SUPER::exportAssetData; push(@{$data->{storage}}, $self->get("storageId")) if ($self->get("storageId") ne ""); return $data; } #------------------------------------------------------------------- =head2 fixUrl ( url ) Extends superclass method to remove periods from post urls =head3 url The url of the post =cut sub fixUrl { my $self = shift; my $url = shift; $url =~ s/\./_/g; $self->SUPER::fixUrl($url); } #------------------------------------------------------------------- =head2 formatContent ( [ content, contentType ]) Formats post content for display. =head3 content The content to format. Defaults to the content in this post. =head3 contentType The content type to use for formatting. Defaults to the content type specified in this post. =cut sub formatContent { my $self = shift; my $content = shift || $self->get("content"); my $contentType = shift || $self->get("contentType"); my $msg = undef ; if (!$self->isa("WebGUI::Asset::Post::Thread")) { # apply appropriate content filter $msg = WebGUI::HTML::filter($content,$self->getThread->getParent->get("replyFilterCode")); } else { $msg = WebGUI::HTML::filter($content,$self->getThread->getParent->get("filterCode")); } $msg = WebGUI::HTML::format($msg, $contentType); if ($self->getThread->getParent->get("useContentFilter")) { $msg = WebGUI::HTML::processReplacements($self->session,$msg); } return $msg; } #------------------------------------------------------------------- =head2 getAutoCommitWorkflowId Overide the master method to return the workflow stored in the parent collaboration system. =cut sub getAutoCommitWorkflowId { my $self = shift; my $cs = $self->getThread->getParent; if ($cs->hasBeenCommitted) { return $cs->get('approvalWorkflow') || $self->session->setting->get('defaultVersionTagWorkflow'); } return undef; } #------------------------------------------------------------------- =head2 getAvatarUrl ( ) Returns a URL to the owner's avatar. =cut sub getAvatarUrl { my $self = shift; my $parent = $self->getThread->getParent; return '' unless $parent and $parent->getValue("avatarsEnabled"); my $user = WebGUI::User->new($self->session, $self->get('ownerUserId')); #Get avatar field, storage Id. my $storageId = $user->profileField("avatar"); return '' unless $storageId; my $avatar = WebGUI::Storage->get($self->session,$storageId); my $avatarUrl = ''; if ($avatar) { #Get url from storage object. foreach my $imageName (@{$avatar->getFiles}) { if ($avatar->isImage($imageName)) { $avatarUrl = $avatar->getUrl($imageName); last; } } } return $avatarUrl; } #------------------------------------------------------------------- =head2 getDeleteUrl ( ) Formats the url to delete a post. =cut sub getDeleteUrl { my $self = shift; return $self->getUrl("func=delete;revision=".$self->get("revisionDate")); } #------------------------------------------------------------------- =head2 getEditUrl ( ) Formats the url to edit a post. =cut sub getEditUrl { my $self = shift; return $self->getUrl("func=edit;revision=".$self->get("revisionDate")); } #------------------------------------------------------------------- =head2 getImageUrl Returns a URL to the first image stored in the storage location for this Post. If there are not stored files, it returns undef. =cut sub getImageUrl { my $self = shift; return undef if ($self->get("storageId") eq ""); my $storage = $self->getStorageLocation; my $url; foreach my $filename (@{$storage->getFiles}) { if ($storage->isImage($filename)) { $url = $storage->getUrl($filename); last; } } return $url; } #------------------------------------------------------------------- =head2 getPosterProfileUrl ( ) Formats the url to view a users profile. =cut sub getPosterProfileUrl { my $self = shift; return WebGUI::User->new($self->session,$self->get("ownerUserId"))->getProfileUrl; } #------------------------------------------------------------------- =head2 getRateUrl ( rating ) Formats the url to rate a post. =head3 rating An integer between 1 and 5 (5 = best). =cut sub getRateUrl { my $self = shift; my $rating = shift; return $self->getUrl("func=rate;rating=".$rating."#id".$self->getId); } #------------------------------------------------------------------- =head2 getReplyUrl ( [ withQuote ] ) Formats the url to reply to a post. =head3 withQuote If specified the reply with automatically quote the parent post. =cut sub getReplyUrl { my $self = shift; my $withQuote = shift || 0; return $self->getUrl("func=add;class=WebGUI::Asset::Post;withQuote=".$withQuote); } #------------------------------------------------------------------- =head2 getStatus Returns the status of this Post, 'approved', 'pending', or 'archived'. =cut sub getStatus { my $self = shift; my $status = $self->get("status"); my $i18n = WebGUI::International->new($self->session,"Asset_Post"); if ($status eq "approved") { return $i18n->get('approved'); } elsif ($status eq "pending") { return $i18n->get('pending'); } elsif ($status eq "archived") { return $i18n->get('archived'); } } #------------------------------------------------------------------- =head2 getStorageLocation Returns a storage location for this Post. If one does not exist, it creates one. =cut sub getStorageLocation { my $self = shift; unless (exists $self->{_storageLocation}) { if ($self->get("storageId") eq "") { $self->{_storageLocation} = WebGUI::Storage->create($self->session); $self->update({storageId=>$self->{_storageLocation}->getId}); } else { $self->{_storageLocation} = WebGUI::Storage->get($self->session,$self->get("storageId")); } } return $self->{_storageLocation}; } #------------------------------------------------------------------- =head2 getSynopsisAndContent ($synopsis, $body) Returns a synopsis taken from the body of the Post, based on either the separator macro, the first html paragraph, or the first physical line of text as defined by newlines. Returns both the synopsis, and the original body content. =head3 $synopsis If passed in, it returns that instead of the calculated synopsis. =head3 $body Body of the Post to use a source for the synopsis. =cut sub getSynopsisAndContent { my $self = shift; my $synopsis = shift; my $body = shift; unless ($synopsis) { my @content; if( $body =~ /\^\-\;/ ) { my @pieces = WebGUI::HTML::splitSeparator($body); $content[0] = shift @pieces; $content[1] = join '', @pieces; } elsif( $body =~ /
/ ) {
@content = WebGUI::HTML::splitTag($body);
}
else {
@content = split("\n",$body);
}
shift @content if $content[0] =~ /^\s*$/;
$synopsis = WebGUI::HTML::filter($content[0],"all");
}
return ($synopsis,$body);
}
#-------------------------------------------------------------------
=head2 getTemplateMetadataVars ( $var )
Append metadata as template variables.
=head3 $var
A hash reference. The template variables will be added to that hash ref.
=cut
sub getTemplateMetadataVars {
my $self = shift;
my $var = shift;
if ($self->session->setting->get("metaDataEnabled")
&& $self->getThread->getParent->get('enablePostMetaData')) {
my $meta = $self->getMetaDataFields();
my @meta_loop = ();
foreach my $field (keys %{ $meta }) {
push @meta_loop, {
value => $meta->{$field}{value},
name => $meta->{$field}{fieldName},
};
my $fieldName = $meta->{$field}{fieldName};
$fieldName =~ tr/ /_/;
$fieldName = lc $fieldName;
$var->{'meta_'.$fieldName.'_value'} = $meta->{$field}{value}; ##By name interface
}
$var->{meta_loop} = \@meta_loop;
}
}
#-------------------------------------------------------------------
=head2 getTemplateVars
Returns a hash reference of template variables for this Post.
=cut
sub getTemplateVars {
my $self = shift;
my $session = $self->session;
my %var = %{$self->get};
my $postUser = WebGUI::User->new($session, $self->get("ownerUserId"));
$var{"userId"} = $self->get("ownerUserId");
$var{"user.isPoster"} = $self->isPoster;
$var{"avatar.url"} = $self->getAvatarUrl;
$var{"userProfile.url"} = $postUser->getProfileUrl($self->getUrl());
$var{"hideProfileUrl" } = $self->get('ownerUserId') eq '1' || $session->user->isVisitor;
$var{"dateSubmitted.human"} = $self->session->datetime->epochToHuman($self->get("creationDate"));
$var{"dateUpdated.human"} = $self->session->datetime->epochToHuman($self->get("revisionDate"));
$var{'title.short'} = $self->chopTitle;
$var{content} = $self->formatContent if ($self->getThread);
$var{'user.canEdit'} = $self->canEdit if ($self->getThread);
$var{"delete.url"} = $self->getDeleteUrl;
$var{"edit.url"} = $self->getEditUrl;
$var{"status"} = $self->getStatus;
$var{"reply.url"} = $self->getReplyUrl;
$var{'reply.withquote.url'} = $self->getReplyUrl(1);
$var{'url'} = $self->getUrl.'#id'.$self->getId;
$var{'url.raw'} = $self->getUrl;
$var{'rating.value'} = $self->get("rating")+0;
$var{'rate.url.thumbsUp'} = $self->getRateUrl(1);
$var{'rate.url.thumbsDown'} = $self->getRateUrl(-1);
$var{'hasRated'} = $self->hasRated;
my $gotImage;
my $gotAttachment;
@{$var{'attachment_loop'}} = ();
unless ($self->get("storageId") eq "") {
my $storage = $self->getStorageLocation;
foreach my $filename (@{$storage->getFiles}) {
if (!$gotImage && $storage->isImage($filename)) {
$var{"image.url"} = $storage->getUrl($filename);
$var{"image.thumbnail"} = $storage->getThumbnailUrl($filename);
$gotImage = 1;
}
if (!$gotAttachment && !$storage->isImage($filename)) {
$var{"attachment.url"} = $storage->getUrl($filename);
$var{"attachment.icon"} = $storage->getFileIconUrl($filename);
$var{"attachment.name"} = $filename;
$gotAttachment = 1;
}
push(@{$var{"attachment_loop"}}, {
url=>$storage->getUrl($filename),
icon=>$storage->getFileIconUrl($filename),
filename=>$filename,
thumbnail=>$storage->getThumbnailUrl($filename),
isImage=>$storage->isImage($filename)
});
}
}
$self->getTemplateMetadataVars(\%var);
return \%var;
}
#-------------------------------------------------------------------
=head2 getThread
Returns the Thread that this Post belongs to. The method caches the result of the Asset creation.
=cut
sub getThread {
my $self = shift;
unless (defined $self->{_thread}) {
my $threadId = $self->get("threadId");
if ($threadId eq "") { # new post
if ($self->getParent->isa("WebGUI::Asset::Wobject::Collaboration")) {
$threadId=$self->getId;
} else {
$threadId=$self->getParent->get("threadId");
}
}
$self->{_thread} = WebGUI::Asset::Post::Thread->new($self->session, $threadId);
}
return $self->{_thread};
}
#-------------------------------------------------------------------
=head2 getThumbnailUrl
If this Post has a storage location, returns a URL to the thumbnail of the first image that
is stored in it. Otherwise, it returns undef.
=cut
sub getThumbnailUrl {
my $self = shift;
return undef if ($self->get("storageId") eq "");
my $storage = $self->getStorageLocation;
my $url;
foreach my $filename (@{$storage->getFiles}) {
if ($storage->isImage($filename)) {
$url = $storage->getThumbnailUrl($filename);
last;
}
}
return $url;
}
#-------------------------------------------------------------------
=head2 hasRated ( )
Returns a boolean indicating whether this user has already rated this post.
=cut
sub hasRated {
my $self = shift;
return 1 if $self->isPoster;
my $flag = 0;
if ($self->session->user->isVisitor) {
($flag) = $self->session->db->quickArray("select count(*) from Post_rating where assetId=? and ipAddress=?",[$self->getId, $self->session->env->getIp]);
} else {
($flag) = $self->session->db->quickArray("select count(*) from Post_rating where assetId=? and userId=?",[$self->getId, $self->session->user->userId]);
}
return $flag;
}
#-------------------------------------------------------------------
=head2 indexContent ( )
Indexing the content of attachments and user defined fields. See WebGUI::Asset::indexContent() for additonal details.
=cut
sub indexContent {
my $self = shift;
my $indexer = $self->SUPER::indexContent;
$indexer->addKeywords($self->get("content"));
$indexer->addKeywords($self->get("userDefined1"));
$indexer->addKeywords($self->get("userDefined2"));
$indexer->addKeywords($self->get("userDefined3"));
$indexer->addKeywords($self->get("userDefined4"));
$indexer->addKeywords($self->get("userDefined5"));
$indexer->addKeywords($self->get("username"));
my $storage = $self->getStorageLocation;
foreach my $file (@{$storage->getFiles}) {
$indexer->addFile($storage->getPath($file));
}
}
#-------------------------------------------------------------------
=head2 incrementViews ( )
Increments the views counter for this post.
=cut
sub incrementViews {
my ($self) = @_;
$self->update({views=>$self->get("views")+1});
}
#-------------------------------------------------------------------
=head2 insertUserPostRating ( rating )
Register the user's rating against this post.
=head3 rating
An integer indicating either thumbss up (+1) or thumbs down (-1)
=cut
sub insertUserPostRating {
my $self = shift;
my $rating = shift;
return undef unless ($rating == -1 || $rating == 1);
return undef if $self->hasRated;
$self->session->db->write("insert into Post_rating (assetId,userId,ipAddress,dateOfRating,rating) values (?,?,?,?,?)",
[$self->getId,
$self->session->user->userId,
$self->session->env->getIp,
time(),
$rating,]
);
}
#-------------------------------------------------------------------
=head2 isNew ( )
Returns a boolean indicating whether this post is new (not an edit).
=cut
sub isNew {
my $self = shift;
return $self->get("creationDate") == $self->get("revisionDate");
}
#-------------------------------------------------------------------
=head2 isPoster ( userId )
Returns a boolean that is true if the current user created this post and is not a visitor.
=cut
sub isPoster {
my $self = shift;
my $userId = shift || $self->session->user->userId;
return ( $userId ne "1" && $userId eq $self->get("ownerUserId") );
}
#-------------------------------------------------------------------
=head2 isReply ( )
Returns a boolean indicating whether this post is a reply.
=cut
sub isReply {
my $self = shift;
return $self->getId ne $self->get("threadId");
}
#-------------------------------------------------------------------
=head2 notifySubscribers ( )
Send notifications to the thread and forum subscribers that a new post has been made.
=cut
sub notifySubscribers {
my $self = shift;
my $i18n = WebGUI::International->new($self->session);
my $var = $self->getTemplateVars();
my $thread = $self->getThread;
my $cs = $thread->getParent;
$cs->appendTemplateLabels($var);
$var->{relativeUrl} = $var->{url};
my $siteurl = $self->session->url->getSiteURL();
$var->{url} = $siteurl.$self->getUrl;
$var->{'notify.subscription.message'} = $i18n->get(875,"Asset_Post");
my $user = WebGUI::User->new($self->session, $self->get("ownerUserId"));
my $setting = $self->session->setting;
my $returnAddress = $setting->get("mailReturnPath");
my $companyAddress = $setting->get("companyEmail");
my $listAddress = $cs->get("mailAddress");
my $posterAddress = $user->getProfileFieldPrivacySetting('email') eq "all"
? $user->profileField('email')
: '';
my $from = $posterAddress || $listAddress || $companyAddress;
my $replyTo = $listAddress || $returnAddress || $companyAddress;
my $sender = $listAddress || $companyAddress || $posterAddress;
my $returnPath = $returnAddress || $sender;
my $listId = $sender;
$listId =~ s/\@/\./;
my $domain = $cs->get("mailAddress");
$domain =~ s/.*\@(.*)/$1/;
my $messageId = "cs-".$self->getId.'@'.$domain;
my $replyId = "";
if ($self->isReply) {
$replyId = "cs-".$self->getParent->getId.'@'.$domain;
}
my $subject = $cs->get("mailPrefix").$self->get("title");
foreach my $subscriptionAsset ($cs, $thread) {
$var->{unsubscribeUrl} = $siteurl.$subscriptionAsset->getUnsubscribeUrl;
$var->{unsubscribeLinkText} = $i18n->get("unsubscribe","Asset_Collaboration");
my $message = $self->processTemplate($var, $cs->get("notificationTemplateId"));
WebGUI::Macro::process($self->session, \$message);
my $groupId = $subscriptionAsset->get('subscriptionGroupId');
my $mail = WebGUI::Mail::Send->create($self->session, {
from=>"<".$from.">",
returnPath => "<".$returnPath.">",
replyTo=>"<".$replyTo.">",
toGroup=>$groupId,
subject=>$subject,
messageId=>'<'.$messageId.'>'
});
if ($self->isReply) {
$mail->addHeaderField("In-Reply-To", "<".$replyId.">");
$mail->addHeaderField("References", "<".$replyId.">");
}
$mail->addHeaderField("List-ID", $cs->getTitle." <".$listId.">");
$mail->addHeaderField("List-Help", " \n\n --- (".$i18n->get('Edited_on')." ".$self->session->datetime->epochToHuman(undef,"%z %Z [GMT%O]")." ".$i18n->get('By')." ".$user->profileField("alias").") --- \n