8 Converts a series of mbox files into moodle backup.zip files
9 ready to be restored into moodle.
19 --templatedir DIRECTORY
23 Penny Leach <penny@she.geek.nz>
24 Julie Pichon <julie.pichon@gmx.com>
33 use Archive
::Zip
qw( :ERROR_CODES :CONSTANTS );
43 use HTML
::Mason
::Compiler
;
44 use Mail
::Box
::Manager
;
46 use Mail
::Message
::Attachment
::Stripper
;
50 use Tie
::Dir
qw(DIR_UNLINK);
52 my $root = $FindBin::Bin
;
56 my $outputdir="$root/output";
57 my $templatedir="$root/templates";
58 my $forumdata = "moddata/forum/8000";
60 my $coursecounter = 0;
67 'sourcefile=s@', \@
$mboxes,
68 'sourcedir=s', \
$sourcedir,
69 'outputdir=s', \
$outputdir,
70 'templatedir=s', \
$templatedir,
73 pod2usage
unless ((scalar(@
{$mboxes}) > 0 or $sourcedir) and $outputdir);
75 $mboxes = [<$sourcedir*>] unless scalar(@
{$mboxes}) > 0;
77 my $mgr = Mail
::Box
::Manager
->new;
79 my $outbuf; # Temp buffer used for writing output
81 'comp_root' => $templatedir,
82 'use_object_files' => 0,
84 'out_method' =>\
$outbuf,
87 my $interp = HTML
::Mason
::Interp
->new(%mason_args);
88 my $compiler = $interp->compiler;
90 foreach my $mbox (@
{$mboxes}) {
97 'id' => $coursecounter++,
98 'name' => basename
($mbox),
103 my $coursedir = "$outputdir/$course->{name}";
105 my $folder = $mgr->open(folder
=> $mbox);
109 my $threader = new Mail
::Thread
($folder->messages('ALL'));
113 for ($threader->rootset) { ### Evaluating |===[%] |
114 sort_out_messages
($_, 0, $course, '', 0, "$coursedir/$forumdata");
117 $folder->close(write => 'NEVER');
119 warn "this course had $dummystarters dummy thread starts "
120 . "(out of " . scalar(@
{$course->{lessons
}}) . ") "
121 . "and I tried to guess the first post.\n" if $dummystarters;
123 # Sort lessons chronologically, so they appear in order on the course page
125 @
{$a->{posts
}}[0]->{timestamp
} <=> @
{$b->{posts
}}[0]->{timestamp
};
127 @
{$course->{lessons
}} = sort by_timestamp @
{$course->{lessons
}};
129 $interp->exec('/moodle.xml', course
=> $course);
130 mkpath
"$coursedir" unless -d
"$coursedir";
131 open my $out, '>', "$coursedir/moodle.xml" or croak
"Output error: $!";
136 my $zip = Archive
::Zip
->new();
137 $zip->addTree("$coursedir", '');
139 unless ( $zip->writeToFileNamed("backup-$course->{name}.zip") == AZ_OK
) {
140 croak
"zip write error ($!)";
142 print "written to backup-$course->{name}.zip\n";
147 sub sort_out_messages
{
148 my ($post, $level, $course, $lesson, $parentid, $path) = @_;
150 if (!$post->message) {
153 my $newtop = guess_top_post
($post->children) if $post->children;
154 if (!$newtop->message) { next; }
156 $post->remove_child($newtop);
158 foreach my $child ($post->children) {
159 $newtop->add_child($child);
165 # the first message in a thread is a special case
166 # it needs to be both a lesson and a forum post.
169 'id' => $lessoncounter++,
170 'name' => strip_subject
($post->message->head->get("Subject")),
171 'body' => parse_body
($post->message),
175 $lesson->{summary
} = (split(/\n\n/, $lesson->{body
}))[0];
177 if (!$lesson->{summary
}) {
178 $lesson->{summary
} = $lesson->{name
};
181 push @
{$course->{lessons
}}, $lesson;
186 'parent' => $parentid,
187 'id' => $postcounter++,
188 'subject' => strip_subject
($post->message->head->get("Subject")),
189 'body' => parse_body
($post->message),
190 'timestamp' => $post->message->timestamp,
193 $newpost->{attachment
} = parse_attachments
($post->message,
194 "$path/$newpost->{id}");
195 push @
{$lesson->{posts
}}, $newpost;
197 sort_out_messages
($post->child, $level+1, $course, $lesson, "$newpost->{id}", $path) if $post->child;
198 sort_out_messages
($post->next, $level, $course, $lesson, "$newpost->{parent}", $path) if $post->next;
202 # Only take relevant content type
207 if ($message->isMultipart()) {
208 foreach my $part ($message->parts) {
209 if ((!defined($part->contentType)) ||
210 (lc($part->contentType) eq 'text/html') ||
211 (lc($part->contentType) eq 'text/plain')) {
212 $body .= $part->decoded->string;
216 my $content_type = $message->get('Content-Type') || 'text/plain';
217 if (!defined($content_type) ||
218 (lc($content_type) eq 'text/html') ||
219 (lc($content_type) eq 'text/plain')) {
220 $body = $message->decoded->string;
227 sub parse_attachments
{
228 my ($message, $path) = @_;
231 my $stripper = Mail
::Message
::Attachment
::Stripper
->new($message);
232 my @attachments = $stripper->attachments;
234 my $count = count_valid_attachments
(@attachments);
237 # Extract attachments from message
238 foreach my $attach (@attachments) {
239 if (($attach->{content_type
} ne 'text/html') &&
240 ($attach->{content_type
} ne 'application/pgp-signature')) {
242 open my $out_file, '>:raw', "$path/$attach->{filename}" or croak
"Output error: ($!)";
243 print $out_file $attach->{payload
};
246 $name = $attach->{filename
};
250 # Moodle only allows 1 attachment/post, if >1 zip them up
252 $name = "attachments.zip";
254 my $zip = Archive
::Zip
->new();
255 $zip->addTree("$path");
257 # If the script is re-run, remove old attachments archive
258 if ($zip->memberNamed($name)) {
259 $zip->removeMember($name);
262 unless ($zip->overwriteAs("$path/$name") == AZ_OK
) {
263 croak
"attachment zip write error ($!)";
266 # Remove unzipped attachments
267 tie
my %dir, 'Tie::Dir', $path, DIR_UNLINK
;
268 foreach (keys %dir) {
269 if (($_ !~ /^$name$/) && ($_ ne ".") && ($_ ne "..")) {
279 sub count_valid_attachments
{
280 my @attachments = @_;
283 foreach my $attach (@attachments) {
284 if (($attach->{content_type
} ne 'text/html') &&
285 ($attach->{content_type
} ne 'application/pgp-signature')) {
305 foreach my $child (@children) {
306 if ($child->message) {
307 if (!$child->isreply) {
308 push @candidates, $child;
313 if (@candidates == 0) {
314 push @candidates, @children;
317 if (@candidates == 1) {
318 $guess = $candidates[0];
322 # Is there a 're:' in the subject?
323 foreach my $cand (@candidates) {
324 if (index(lc($cand->message->head->get("Subject")), 're:') == -1) {
330 push @round2, @children;
336 # Last resort, pick the one with the oldest timestamp
337 my $oldest = pop @round2;
338 foreach my $msg (@round2) {
339 if ($msg->message->timestamp < $oldest->message->timestamp) {