[system] / trunk / webwork2 / lib / WeBWorK / ContentGenerator / Instructor / SendMail.pm Repository:
ViewVC logotype

Annotation of /trunk/webwork2/lib/WeBWorK/ContentGenerator/Instructor/SendMail.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 6936 - (view) (download) (as text)

1 : sh002i 1663 ################################################################################
2 :     # WeBWorK Online Homework Delivery System
3 : sh002i 5319 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
4 : apizer 5727 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Instructor/SendMail.pm,v 1.64 2007/08/13 22:59:55 sh002i Exp $
5 : sh002i 1663 #
6 :     # This program is free software; you can redistribute it and/or modify it under
7 :     # the terms of either: (a) the GNU General Public License as published by the
8 :     # Free Software Foundation; either version 2, or (at your option) any later
9 :     # version, or (b) the "Artistic License" which comes with this package.
10 :     #
11 :     # This program is distributed in the hope that it will be useful, but WITHOUT
12 :     # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 :     # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
14 :     # Artistic License for more details.
15 :     ################################################################################
16 :    
17 : gage 1368 package WeBWorK::ContentGenerator::Instructor::SendMail;
18 :     use base qw(WeBWorK::ContentGenerator::Instructor);
19 :    
20 :     =head1 NAME
21 :    
22 :     WeBWorK::ContentGenerator::Instructor::SendMail - Entry point for User-specific data editing
23 :    
24 :     =cut
25 :    
26 :     use strict;
27 :     use warnings;
28 : gage 4234 #use CGI qw(-nosticky );
29 :     use WeBWorK::CGI;
30 : sh002i 4879 use Email::Address;
31 : gage 4234 use HTML::Entities;
32 : gage 1373 use Mail::Sender;
33 : sh002i 4596 use Socket qw/unpack_sockaddr_in inet_ntoa/; # for remote host/port info
34 : gage 3329 use Text::Wrap qw(wrap);
35 : mschmitt 2203 use WeBWorK::HTML::ScrollingRecordList qw/scrollingRecordList/;
36 : sh002i 4741 use WeBWorK::Utils qw/readFile readDirectory/;
37 : mschmitt 2207 use WeBWorK::Utils::FilterRecords qw/filterRecords/;
38 : gage 1368
39 : sh002i 4596 use mod_perl;
40 :     use constant MP2 => ( exists $ENV{MOD_PERL_API_VERSION} and $ENV{MOD_PERL_API_VERSION} >= 2 );
41 :    
42 : gage 3041 #my $REFRESH_RESIZE_BUTTON = "Set preview to: "; # handle submit value idiocy
43 :     my $UPDATE_SETTINGS_BUTTON = "Update settings and refresh page"; # handle submit value idiocy
44 : gage 1368 sub initialize {
45 :     my ($self) = @_;
46 : gage 1928 my $r = $self->r;
47 :     my $db = $r->db;
48 :     my $ce = $r->ce;
49 :     my $authz = $r->authz;
50 :     my $user = $r->param('user');
51 : gage 1368
52 : mschmitt 2207 my @selected_filters;
53 :     if (defined ($r->param('classList!filter'))){ @selected_filters = $r->param('classList!filter');}
54 :     else {@selected_filters = ("all");}
55 :    
56 : toenail 2320
57 :     # Check permissions
58 :     return unless $authz->hasPermissions($user, "access_instructor_tools");
59 :     return unless $authz->hasPermissions($user, "send_mail");
60 :    
61 : sh002i 4490 #############################################################################################
62 :     # gather directory data
63 :     #############################################################################################
64 : gage 1369 my $emailDirectory = $ce->{courseDirs}->{email};
65 :     my $scoringDirectory = $ce->{courseDirs}->{scoring};
66 :     my $templateDirectory = $ce->{courseDirs}->{templates};
67 : gage 1368
68 : mschmitt 2203 my $action = $r->param('action') ;
69 : gage 1369 my $openfilename = $r->param('openfilename');
70 :     my $savefilename = $r->param('savefilename');
71 : gage 1370
72 :    
73 :     #FIXME get these values from global course environment (see subroutines as well)
74 :     my $default_msg_file = 'default.msg';
75 :     my $old_default_msg_file = 'old_default.msg';
76 :    
77 : gage 1372
78 : dpvc 2388 # get user record
79 :     my $ur = $self->{db}->getUser($user);
80 :    
81 : gage 1370 # store data
82 : sh002i 4879 $self->{defaultFrom} = $ur->rfc822_mailbox;
83 :     $self->{defaultReply} = $ur->rfc822_mailbox;
84 : dpvc 2388 $self->{defaultSubject} = $self->r->urlpath->arg("courseID") . " notice";
85 :    
86 : gage 1370 $self->{rows} = (defined($r->param('rows'))) ? $r->param('rows') : $ce->{mail}->{editor_window_rows};
87 :     $self->{columns} = (defined($r->param('columns'))) ? $r->param('columns') : $ce->{mail}->{editor_window_columns};
88 :     $self->{default_msg_file} = $default_msg_file;
89 :     $self->{old_default_msg_file} = $old_default_msg_file;
90 : gage 1372 $self->{merge_file} = (defined($r->param('merge_file' ))) ? $r->param('merge_file') : 'None';
91 : gage 3041 #$self->{preview_user} = (defined($r->param('preview_user'))) ? $r->param('preview_user') : $user;
92 :     # an expermiment -- share the scrolling list for preivew and sendTo actions.
93 :     my @classList = (defined($r->param('classList'))) ? $r->param('classList') : ($user);
94 :     $self->{preview_user} = $classList[0] || $user;
95 : gage 1372
96 : sh002i 4490 #############################################################################################
97 :     # gather database data
98 :     #############################################################################################
99 : gage 1372 # FIXME this might be better done in body? We don't always need all of this data. or do we?
100 : sh002i 4518 # DBFIXME shouldn't need ID list
101 :     # DBFIXME do filtering in database
102 : gage 1773 my @users = $db->listUsers;
103 : mschmitt 2203 my @Users = $db->getUsers(@users);
104 : sh002i 4452 # filter out users who don't get included in email (fixes bug #938)
105 :     @Users = grep { $ce->status_abbrev_has_behavior($_->status, "include_in_email") } @Users;
106 : gage 1372 my @user_records = ();
107 : mschmitt 2203
108 : sh002i 4490 ## Mark's code to prefilter userlist
109 : sh002i 4518 # DBFIXME more filtering that we can do in the database
110 : mschmitt 2203
111 :    
112 :     my (@viewable_sections,@viewable_recitations);
113 :    
114 :     if (defined @{$ce->{viewable_sections}->{$user}})
115 :     {@viewable_sections = @{$ce->{viewable_sections}->{$user}};}
116 :     if (defined @{$ce->{viewable_recitations}->{$user}})
117 :     {@viewable_recitations = @{$ce->{viewable_recitations}->{$user}};}
118 :    
119 :     if (@viewable_sections or @viewable_recitations){
120 :     foreach my $student (@Users){
121 :     my $keep = 0;
122 :     foreach my $sec (@viewable_sections){
123 :     if ($student->section() eq $sec){$keep = 1;}
124 :     }
125 :     foreach my $rec (@viewable_recitations){
126 :     if ($student->recitation() eq $rec){$keep = 1;}
127 :     }
128 :     if ($keep) {push @user_records, $student;}
129 :     }
130 : gage 1667 }
131 : mschmitt 2203 else {@user_records = @Users;}
132 :    
133 : sh002i 4490 ## End Mark's code
134 : gage 1372
135 : gage 1720
136 :     # replace the user names by a sorted version.
137 :     @users = map {$_->user_id} @user_records;
138 : gage 1372 # store data
139 :     $self->{ra_users} = \@users;
140 :     $self->{ra_user_records} = \@user_records;
141 :    
142 : sh002i 4490 #############################################################################################
143 :     # gather list of recipients
144 :     #############################################################################################
145 : gage 1372 my @send_to = ();
146 :     #FIXME this (radio) is a lousy name
147 :     my $recipients = $r->param('radio');
148 : gage 1375 if (defined($recipients) and $recipients eq 'all_students') { #only active students #FIXME status check??
149 : mschmitt 2207
150 : sh002i 4490 ## Add code so that only people who pass the current filters are added to our list of recipients.
151 :     # @user_records = filterRecords({filter=\@selected_filters},@user_records);
152 :     # I wasn't able to make this work
153 :     # I edited the selection button to make that clear.
154 :     #
155 : mschmitt 2207
156 : gage 1372 foreach my $ur (@user_records) {
157 : sh002i 3690 push(@send_to,$ur->user_id)
158 :     if $ce->status_abbrev_has_behavior($ur->status, "include_in_email")
159 :     and not $ur->user_id =~ /practice/;
160 : gage 1372 }
161 : gage 1375 } elsif (defined($recipients) and $recipients eq 'studentID' ) {
162 : gage 1372 @send_to = $r->param('classList');
163 :     } else {
164 : gage 1375 # no recipients have been defined -- probably the first time on the page
165 : gage 1372 }
166 :     $self->{ra_send_to} = \@send_to;
167 : sh002i 4490 #################################################################
168 :     # Check the validity of the input file name
169 :     #################################################################
170 : gage 1370 my $input_file = '';
171 :     #make sure an input message file was submitted and exists
172 :     #else use the default message
173 :     if ( defined($openfilename) ) {
174 :     if ( -e "${emailDirectory}/$openfilename") {
175 :     if ( -R "${emailDirectory}/$openfilename") {
176 :     $input_file = $openfilename;
177 : gage 1369 } else {
178 : toenail 2320 $self->addbadmessage(CGI::p(join("",
179 : gage 1370 "The file ${emailDirectory}/$openfilename is not readable by the webserver.",CGI::br(),
180 :     "Check that it's permissions are set correctly.",
181 : toenail 2320 )));
182 : gage 1369 }
183 :     } else {
184 : gage 1370 $input_file = $default_msg_file;
185 : toenail 2320 $self->addbadmessage(CGI::p(join("",
186 : gage 1370 "The file ${emailDirectory}/$openfilename cannot be found.",CGI::br(),
187 :     "Check whether it exists and whether the directory $emailDirectory can be read by the webserver.",CGI::br(),
188 :     "Using contents of the default message $default_msg_file instead.",
189 : toenail 2320 )));
190 : gage 1369 }
191 : gage 1370 } else {
192 : gage 1372 $input_file = $default_msg_file;
193 : gage 1370 }
194 : gage 1372 $self->{input_file} =$input_file;
195 : gage 1370
196 : sh002i 4490 #################################################################
197 :     # Determine the file name to save message into
198 :     #################################################################
199 : gage 1371 my $output_file = 'FIXME no output file specified';
200 : gage 1370 if (defined($action) and $action eq 'Save as Default') {
201 :     $output_file = $default_msg_file;
202 : toenail 2084 } elsif ( defined($action) and ($action =~/save/i)) {
203 :     if (defined($savefilename) and $savefilename ) {
204 :     $output_file = $savefilename;
205 :     } else {
206 : toenail 2320 $self->addbadmessage(CGI::p("No filename was specified for saving! The message was not saved."));
207 : toenail 2084 }
208 : gage 1370 } elsif ( defined($input_file) ) {
209 :     $output_file = $input_file;
210 :     }
211 : gage 1953
212 : gage 1370 #################################################################
213 :     # Sanity check on save file name
214 :     #################################################################
215 :    
216 :     if ($output_file =~ /^[~.]/ || $output_file =~ /\.\./) {
217 : toenail 2320 $self->addbadmessage(CGI::p("For security reasons, you cannot specify a message file from a directory",
218 : toenail 2084 "higher than the email directory (you can't use ../blah/blah for example). ",
219 : toenail 2320 "Please specify a different file or move the needed file to the email directory",));
220 : toenail 2084 }
221 : gage 1371 unless ($output_file =~ m|\.msg$| ) {
222 : toenail 2320 $self->addbadmessage(CGI::p("Invalid file name.",
223 : gage 1371 "The file name \"$output_file\" does not have a \".msg\" extension",
224 :     "All email file names must end in the extension \".msg\"",
225 :     "choose a file name with a \".msg\" extension.",
226 : toenail 2320 "The message was not saved.",));
227 : gage 1371 }
228 : toenail 2084
229 : gage 1371 $self->{output_file} = $output_file; # this is ok. It will be put back in the text input box for re-editing.
230 : gage 1370
231 : toenail 2084
232 : sh002i 4490 #############################################################################################
233 :     # Determine input source
234 :     #############################################################################################
235 : mschmitt 2203 #warn "Action = $action";
236 :     my $input_source;
237 :     if ($action){
238 :     $input_source = ( defined( $r->param('body') ) and $action ne 'Open' ) ? 'form' : 'file';}
239 :     else { $input_source = ( defined($r->param('body')) ) ? 'form' : 'file';}
240 : gage 1953
241 : sh002i 4490 #############################################################################################
242 :     # Get inputs
243 :     #############################################################################################
244 : gage 1370 my($from, $replyTo, $r_text, $subject);
245 :     if ($input_source eq 'file') {
246 : gage 1953
247 : gage 1370 ($from, $replyTo,$subject,$r_text) = $self->read_input_file("$emailDirectory/$input_file");
248 :    
249 : gage 1953
250 : gage 1370 } elsif ($input_source eq 'form') {
251 :     # read info from the form
252 :     # bail if there is no message body
253 : gage 1369
254 : gage 1370 $from = $r->param('from');
255 :     $replyTo = $r->param('replyTo');
256 :     $subject = $r->param('subject');
257 : gage 3329 my $body = $r->param('body');
258 : gage 1370 # Sanity check: body must contain non-white space
259 : toenail 2320 $self->addbadmessage(CGI::p('You didn\'t enter any message.')) unless ($r->param('body') =~ /\S/);
260 : gage 1370 $r_text = \$body;
261 : gage 1369
262 : gage 1370 }
263 : sh002i 4596
264 :     my $remote_host;
265 :     if (MP2) {
266 :     $remote_host = $r->connection->remote_addr->ip_get || "UNKNOWN";
267 :     } else {
268 :     (undef, $remote_host) = unpack_sockaddr_in($r->connection->remote_addr);
269 :     $remote_host = defined $remote_host ? inet_ntoa($remote_host) : "UNKNOWN";
270 :     }
271 :    
272 : gage 1370 # store data
273 :     $self->{from} = $from;
274 :     $self->{replyTo} = $replyTo;
275 :     $self->{subject} = $subject;
276 : sh002i 4596 $self->{remote_host} = $remote_host;
277 : gage 1370 $self->{r_text} = $r_text;
278 :    
279 :    
280 : gage 1372
281 : sh002i 4490 ###################################################################################
282 :     #Determine the appropriate script action from the buttons
283 :     ###################################################################################
284 :     # first time actions
285 :     # open new file
286 :     # open default file
287 :     # choose merge file actions
288 :     # chose merge button
289 :     # option actions
290 :     # 'reset rows'
291 :    
292 :     # save actions
293 :     # "save" button
294 :     # "save as" button
295 :     # "save as default" button
296 :     # preview actions
297 :     # 'preview' button
298 :     # email actions
299 :     # 'entire class'
300 :     # 'selected studentIDs'
301 :     # error actions (various)
302 : gage 1372
303 :    
304 : sh002i 4490 #############################################################################################
305 :     # if no form is submitted, gather data needed to produce the mail form and return
306 :     #############################################################################################
307 : gage 1372 my $to = $r->param('To');
308 :     my $script_action = '';
309 :    
310 :    
311 : gage 3041 if(not defined($action) or $action eq 'Open'
312 :     or $action eq $UPDATE_SETTINGS_BUTTON ){
313 : gage 1953
314 : gage 1369 return '';
315 :     }
316 :    
317 :    
318 :    
319 :    
320 :    
321 : sh002i 4490 #############################################################################################
322 :     # If form is submitted deal with filled out forms
323 :     # and various actions resulting from different buttons
324 :     #############################################################################################
325 : gage 1369
326 : gage 1372
327 : gage 1370 if ($action eq 'Save' or $action eq 'Save as:' or $action eq 'Save as Default') {
328 :    
329 : sh002i 4490 #warn "FIXME Saving files action = $action outputFileName=$output_file";
330 : gage 1369
331 : gage 1370 #################################################################
332 :     # construct message body
333 :     #################################################################
334 :     my $temp_body = ${ $r_text };
335 :     $temp_body =~ s/\r\n/\n/g;
336 :     $temp_body = join("",
337 :     "From: $from \nReply-To: $replyTo\n" ,
338 :     "Subject: $subject\n" ,
339 :     "Message: \n $temp_body");
340 : sh002i 4490 #warn "FIXME from $from | subject $subject |reply $replyTo|msg $temp_body";
341 : gage 1370 #################################################################
342 :     # overwrite protection
343 :     #################################################################
344 :     if ($action eq 'Save as:' and -e "$emailDirectory/$output_file") {
345 : toenail 2320 $self->addbadmessage(CGI::p("The file $emailDirectory/$output_file already exists and cannot be overwritten",
346 :     "The message was not saved"));
347 : toenail 2084 return;
348 : gage 1370 }
349 :    
350 :     #################################################################
351 :     # Back up existing file?
352 :     #################################################################
353 : gage 1371 if ($action eq 'Save as Default' and -e "$emailDirectory/$default_msg_file") {
354 :     rename("$emailDirectory/$default_msg_file","$emailDirectory/$old_default_msg_file") or
355 :     die "Can't rename $emailDirectory/$default_msg_file to $emailDirectory/$old_default_msg_file ",
356 :     "Check permissions for webserver on directory $emailDirectory. $!";
357 : toenail 2320 $self->addgoodmessage(CGI::p("Backup file <code>$emailDirectory/$old_default_msg_file</code> created." . CGI::br()));
358 : gage 1370 }
359 :     #################################################################
360 :     # Save the message
361 :     #################################################################
362 : toenail 2084 $self->saveProblem($temp_body, "${emailDirectory}/$output_file" ) unless ($output_file =~ /^[~.]/ || $output_file =~ /\.\./ || not $output_file =~ m|\.msg$|);
363 :     unless ( $self->{submit_message} or not -w "${emailDirectory}/$output_file" ) { # if there are no errors report success
364 : toenail 2320 $self->addgoodmessage(CGI::p("Message saved to file <code>${emailDirectory}/$output_file</code>."));
365 : gage 1953 }
366 :    
367 : gage 2050 } elsif ($action eq 'Preview message') {
368 : gage 1372 $self->{response} = 'preview';
369 : gage 1370
370 :     } elsif ($action eq 'Send Email') {
371 : sh002i 4879 # verify format of From address (one valid rfc2822 address)
372 :     my @parsed_from_addrs = Email::Address->parse($self->{from});
373 :     unless (@parsed_from_addrs == 1) {
374 :     $self->addbadmessage("From field must contain one valid email address.");
375 :     return;
376 :     }
377 :    
378 :     # verify format of Reply-to address (zero or more valid rfc2822 addresses)
379 :     if (defined $self->{replyTo} and $self->{replyTo} ne "") {
380 :     my @parsed_replyto_addrs = Email::Address->parse($self->{replyTo});
381 :     unless (@parsed_replyto_addrs > 0) {
382 :     $self->addbadmessage("Invalid Reply-to address.");
383 :     return;
384 :     }
385 :     }
386 :    
387 : gage 3329 # check that recipients have been selected.
388 : gage 1373 my @recipients = @{$self->{ra_send_to}};
389 : sh002i 4879 unless (@recipients) {
390 : sh002i 4868 $self->addbadmessage(CGI::p("No recipients selected. Please select one or more recipients from the list below."));
391 : sh002i 4879 return;
392 : gage 3611 }
393 : sh002i 4879
394 :     # get merge file
395 :     my $merge_file = ( defined($self->{merge_file}) ) ? $self->{merge_file} : 'None';
396 :     my $delimiter = ',';
397 :     my $rh_merge_data = $self->read_scoring_file("$merge_file", "$delimiter");
398 :     unless (ref($rh_merge_data) ) {
399 :     $self->addbadmessage(CGI::p("No merge data file"));
400 :     $self->addbadmessage(CGI::p("Can't read merge file $merge_file. No message sent"));
401 :     return;
402 :     } ;
403 :     $self->{rh_merge_data} = $rh_merge_data;
404 :    
405 :     # we don't set the response until we're sure that email can be sent
406 :     $self->{response} = 'send_email';
407 :    
408 :     # FIXME i'm not sure why we're pulling this out here -- mail_message_to_recipients does have
409 :     # access to the course environment and should just grab it directly
410 :     $self->{smtpServer} = $ce->{mail}->{smtpServer};
411 :    
412 :     # do actual mailing in the cleanup phase, since it could take a long time
413 :     # FIXME we need to do a better job providing status notifications for long-running email jobs
414 :     my $post_connection_action = sub {
415 :     my $r = shift;
416 : sh002i 4880 # catch exceptions generated during the sending process
417 :     my $result_message = eval { $self->mail_message_to_recipients() };
418 :     if ($@) {
419 :     # add the die message to the result message
420 :     $result_message = "An error occurred while trying to send email.\n"
421 :     . "The error message is:\n\n$@\n\n";
422 :     # and also write it to the apache log
423 :     $self->r->log->error("An error occurred while trying to send email: $@");
424 :     }
425 :     # this could fail too...
426 :     eval { $self->email_notification($result_message) };
427 :     if ($@) {
428 :     $self->r->log->error("An error occured while trying to send the email notification: $@");
429 :     }
430 : sh002i 4879 };
431 :     if (MP2) {
432 :     $r->connection->pool->cleanup_register($post_connection_action);
433 :     } else {
434 :     $r->post_connection($post_connection_action);
435 :     }
436 : gage 1370 } else {
437 : toenail 2320 $self->addbadmessage(CGI::p("Didn't recognize button $action"));
438 : gage 1370 }
439 : gage 1369
440 :    
441 : toenail 2084
442 : gage 1369 } #end initialize
443 :    
444 : gage 1368
445 :    
446 :    
447 : gage 1928
448 : gage 1368 sub body {
449 : gage 1928 my ($self) = @_;
450 :     my $r = $self->r;
451 :     my $urlpath = $r->urlpath;
452 : toenail 2320 my $authz = $r->authz;
453 : gage 1928 my $setID = $urlpath->arg("setID");
454 : gage 1372 my $response = (defined($self->{response}))? $self->{response} : '';
455 : toenail 2320 my $user = $r->param('user');
456 :    
457 :     # Check permissions
458 :     return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to access instructor tools"))
459 :     unless $authz->hasPermissions($user, "access_instructor_tools");
460 :    
461 :     return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to send mail to students"))
462 :     unless $authz->hasPermissions($user, "send_mail");
463 :    
464 : gage 1372 if ($response eq 'preview') {
465 :     $self->print_preview($setID);
466 : sh002i 4868 } elsif ($response eq 'send_email' and $self->{ra_send_to} and @{$self->{ra_send_to}}){
467 :     my $message = CGI::i("Email is being sent to ". scalar(@{$self->{ra_send_to}})." recipient(s). You will be notified"
468 : sh002i 4780 ." by email when the task is completed. This may take several minutes if the class is large."
469 :     );
470 :     $self->addgoodmessage($message);
471 :     $self->{message} .= $message;
472 :    
473 :     $self->print_form($setID);
474 : gage 1375 } else {
475 :     $self->print_form($setID);
476 : gage 1372 }
477 :    
478 :     }
479 : sh002i 4741
480 : gage 1372 sub print_preview {
481 : gage 1928 my ($self) = @_;
482 :     my $r = $self->r;
483 :     my $urlpath = $r->urlpath;
484 :     my $setID = $urlpath->arg("setID");
485 :    
486 : gage 1372 # get preview user
487 : gage 1928 my $ur = $r->db->getUser($self->{preview_user}); #checked
488 : gage 1667 die "record for preview user ".$self->{preview_user}. " not found." unless $ur;
489 : gage 1372
490 :     # get merge file
491 :     my $merge_file = ( defined($self->{merge_file}) ) ? $self->{merge_file} : 'None';
492 :     my $delimiter = ',';
493 : gage 1397 my $rh_merge_data = $self->read_scoring_file("$merge_file", "$delimiter");
494 : gage 1372
495 : sh002i 4596 my ($msg, $preview_header) = $self->process_message($ur,$rh_merge_data,1); # 1 == for preview
496 : gage 1372
497 :     my $recipients = join(" ",@{$self->{ra_send_to} });
498 : toenail 2084 my $errorMessage = defined($self->{submit_message}) ? CGI::i($self->{submit_message} ) : '' ;
499 : apizer 5727
500 :     # Format message keeping the preview_header lined up
501 :     $errorMessage = wrap("","",$errorMessage);
502 :     $msg = wrap("","",$msg);
503 :    
504 : gage 1373 $msg = join("",
505 :     $errorMessage,
506 :     $preview_header,
507 :     "To: " , $ur->email_address,"\n",
508 :     "From: " , $self->{from} , "\n" ,
509 :     "Reply-To: " , $self->{replyTo} , "\n" ,
510 :     "Subject: " , $self->{subject} , "\n" ,"\n" ,
511 :     $msg , "\n"
512 :     );
513 : gage 1372
514 : apizer 5727 # return join("", '<pre>',wrap("","",$msg),"\n","\n",
515 :     return join("", '<pre>',$msg,"\n","\n",
516 : gage 1372 '</pre>',
517 :     CGI::p('Use browser back button to return from preview mode'),
518 :     CGI::h3('Emails to be sent to the following:'),
519 :     $recipients, "\n",
520 :    
521 :     );
522 :    
523 :     }
524 :     sub print_form {
525 : gage 1928 my ($self) = @_;
526 :     my $r = $self->r;
527 :     my $urlpath = $r->urlpath;
528 :     my $authz = $r->authz;
529 :     my $db = $r->db;
530 :     my $ce = $r->ce;
531 :     my $courseName = $urlpath->arg("courseID");
532 :     my $setID = $urlpath->arg("setID");
533 :     my $user = $r->param('user');
534 :    
535 : gage 1938 my $root = $ce->{webworkURLs}->{root};
536 : gage 6936 my $sendMailPage = $urlpath->newFromModule($urlpath->module, $r, courseID=>$courseName);
537 : gage 1938 my $sendMailURL = $self->systemLink($sendMailPage, authen => 0);
538 : gage 1368
539 : toenail 2084 return CGI::em("You are not authorized to access the Instructor tools.") unless $authz->hasPermissions($user, "access_instructor_tools");
540 : gage 1368
541 :     my $userTemplate = $db->newUser;
542 :     my $permissionLevelTemplate = $db->newPermissionLevel;
543 :    
544 :     # This code will require changing if the permission and user tables ever have different keys.
545 : gage 3041 my @users = sort @{ $self->{ra_users} };
546 : gage 1372 my $ra_user_records = $self->{ra_user_records};
547 :     my %classlistLabels = ();# %$hr_classlistLabels;
548 :     foreach my $ur (@{ $ra_user_records }) {
549 : gage 1720 $classlistLabels{$ur->user_id} = $ur->user_id.': '.$ur->last_name. ', '. $ur->first_name.' -- '.$ur->section." / ".$ur->recitation;
550 : gage 1372 }
551 : gage 1368
552 : sh002i 4490 ## Mark edit define scrolling list
553 : mschmitt 2203 my $scrolling_user_list = scrollingRecordList({
554 :     name => "classList", ## changed from classList to action
555 :     request => $r,
556 :     default_sort => "lnfn",
557 :     default_format => "lnfn_uid",
558 :     default_filters => ["all"],
559 :     size => 5,
560 :     multiple => 1,
561 : gage 3041 refresh_button_name =>'Update settings and refresh page',
562 : mschmitt 2203 }, @{$ra_user_records});
563 : gage 1368
564 : sh002i 4490 ##############################################################################################################
565 : gage 1368
566 : gage 1372
567 : gage 1369 my $from = $self->{from};
568 :     my $subject = $self->{subject};
569 :     my $replyTo = $self->{replyTo};
570 :     my $columns = $self->{columns};
571 :     my $rows = $self->{rows};
572 : gage 1370 my $text = defined($self->{r_text}) ? ${ $self->{r_text} }: 'FIXME no text was produced by initialization!!';
573 :     my $input_file = $self->{input_file};
574 :     my $output_file = $self->{output_file};
575 : gage 1371 my @sorted_messages = $self->get_message_file_names;
576 :     my @sorted_merge_files = $self->get_merge_file_names;
577 :     my $merge_file = ( defined($self->{merge_file}) ) ? $self->{merge_file} : 'None';
578 : gage 1372 my $delimiter = ',';
579 : gage 1397 my $rh_merge_data = $self->read_scoring_file("$merge_file", "$delimiter");
580 : gage 1372 my @merge_keys = keys %$rh_merge_data;
581 :     my $preview_user = $self->{preview_user};
582 : gage 1667 my $preview_record = $db->getUser($preview_user); # checked
583 :     die "record for preview user ".$self->{preview_user}. " not found." unless $preview_record;
584 : gage 1372
585 : gage 1667
586 : sh002i 4490 #############################################################################################
587 : gage 1372
588 : gage 1938 print CGI::start_form({method=>"post", action=>$sendMailURL});
589 : gage 1372 print $self->hidden_authen_fields();
590 : sh002i 4490 #############################################################################################
591 :     # begin upper table
592 :     #############################################################################################
593 : gage 1372
594 : gage 1368 print CGI::start_table({-border=>'2', -cellpadding=>'4'});
595 : gage 1720 print CGI::Tr({-align=>'left',-valign=>'top'},
596 : sh002i 4490 #############################################################################################
597 :     # first column
598 :     #############################################################################################
599 : gage 1372
600 : gage 4269 CGI::td({},
601 :     CGI::strong("Message file: "), $input_file,"\n",CGI::br(),
602 : gage 1371 CGI::submit(-name=>'action', -value=>'Open'), '&nbsp;&nbsp;&nbsp;&nbsp;',"\n",
603 :     CGI::popup_menu(-name=>'openfilename',
604 :     -values=>\@sorted_messages,
605 :     -default=>$input_file
606 : gage 3041 ),
607 :     "\n",CGI::br(),
608 :     CGI::strong("Save file to: "), $output_file,
609 :     "\n",CGI::br(),
610 :     CGI::strong('Merge file: '), $merge_file,
611 :     CGI::br(),
612 :     CGI::popup_menu(-name=>'merge_file',
613 :     -values=>\@sorted_merge_files,
614 :     -default=>$merge_file,
615 :     ), "\n",
616 :     "\n",
617 :     #CGI::hr(),
618 :     CGI::div({style=>"background-color: #CCCCCC"},
619 :     "\n", 'From:','&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;', CGI::textfield(-name=>"from", -size=>30, -value=>$from, -override=>1),
620 :     "\n", CGI::br(),'Reply-To: ', CGI::textfield(-name=>"replyTo", -size=>30, -value=>$replyTo, -override=>1),
621 : gage 4234 "\n", CGI::br(),'Subject: ', CGI::br(), CGI::textarea(-name=>'subject', -default=>$subject, -rows=>3,-cols=>30, -override=>1),
622 : gage 3041 ),
623 :     #CGI::hr(),
624 : sh002i 4741 "Editor rows: ", CGI::textfield(-name=>'rows', -size=>3, -value=>$rows),
625 :     " columns: ", CGI::textfield(-name=>'columns', -size=>3, -value=>$columns),
626 :     CGI::br(),
627 : gage 3041 CGI::submit(-name=>'action', -value=>$UPDATE_SETTINGS_BUTTON),
628 :    
629 : gage 1370 ),
630 : sh002i 4490 #############################################################################################
631 :     # second column
632 :     #############################################################################################
633 : gage 4269
634 : sh002i 4490 ## Edit by Mark to insert scrolling list
635 : gage 4269 CGI::td({-style=>"width:33%"},
636 :     CGI::strong("Send to:"),
637 :     CGI::radio_group(-name=>'radio',
638 :     -values=>['all_students','studentID'],
639 :     -labels=>{all_students=>'All students in course',studentID => 'Selected students'},
640 :     -default=>'studentID', -linebreak=>0),
641 : gage 3041 CGI::br(),$scrolling_user_list,
642 : apizer 5727 CGI::i("Preview set to: "), $preview_record->last_name,'(', $preview_record->user_id,')',
643 : gage 3041 CGI::submit(-name=>'action', -value=>'preview',-label=>'Preview message'),'&nbsp;&nbsp;',
644 :     ),
645 : sh002i 4741 ); # end Tr
646 :    
647 :     # second row, for reference popup menu
648 :     print CGI::Tr(
649 :     CGI::td({align=>'center',colspan=>2},
650 :    
651 : gage 3041
652 : sh002i 4490 #CGI::i('Press any action button to update display'),CGI::br(),
653 : gage 1370 #show available macros
654 :     CGI::popup_menu(
655 :     -name=>'dummyName',
656 :     -values=>['', '$SID', '$FN', '$LN', '$SECTION', '$RECITATION','$STATUS', '$EMAIL', '$LOGIN', '$COL[3]', '$COL[-1]'],
657 :     -labels=>{''=>'list of insertable macros',
658 :     '$SID'=>'$SID - Student ID',
659 :     '$FN'=>'$FN - First name',
660 :     '$LN'=>'$LN - Last name',
661 : gage 1372 '$SECTION'=>'$SECTION',
662 : gage 1370 '$RECITATION'=>'$RECITATION',
663 :     '$STATUS'=>'$STATUS - C, Audit, Drop, etc.',
664 :     '$EMAIL'=>'$EMAIL - Email address',
665 :     '$LOGIN'=>'$LOGIN - Login',
666 : gage 1372 '$COL[3]'=>'$COL[3] - 3rd col',
667 : gage 1370 '$COL[-1]'=>'$COL[-1] - Last column'
668 :     }
669 :     ), "\n",
670 :     ),
671 : sh002i 4741 );
672 :    
673 : gage 1372 print CGI::end_table();
674 : sh002i 4490 #############################################################################################
675 :     # end upper table
676 :     #############################################################################################
677 : gage 1372
678 : sh002i 4490 #############################################################################################
679 :     # merge file fragment and message text area field
680 :     #############################################################################################
681 :     my @tmp2;
682 :     eval{ @tmp2= @{$rh_merge_data->{ $db->getUser($preview_user)->student_id } };}; # checked
683 :     if ($@ and $merge_file ne 'None') {
684 :     print "No merge data for $preview_user in merge file: &lt;$merge_file&gt;",CGI::br();
685 :     } else {
686 :     print CGI::pre("",data_format(1..($#tmp2+1)),"<br>", data_format2(@tmp2));
687 :     }
688 :     #create a textbox with the subject and a textarea with the message
689 :     #print actual body of message
690 : gage 1369
691 : gage 1370 print "\n", CGI::p( $self->{message}) if defined($self->{message});
692 : gage 4234 print "\n", CGI::p( CGI::textarea(-name=>'body', -default=>$text, -rows=>$rows, -cols=>$columns, -override=>1));
693 : gage 1372
694 : sh002i 4490 #############################################################################################
695 :     # action button table
696 :     #############################################################################################
697 : gage 1370 print CGI::table( { -border=>2,-cellpadding=>4},
698 : gage 4269 CGI::Tr( {},
699 :     CGI::td({}, CGI::submit(-name=>'action', -value=>'Send Email') ), "\n",
700 :     CGI::td({}, CGI::submit(-name=>'action', -value=>'Save')," to $output_file"), " \n",
701 :     CGI::td({}, CGI::submit(-name=>'action', -value=>'Save as:'),
702 : gage 1370 CGI::textfield(-name=>'savefilename', -size => 20, -value=> "$output_file", -override=>1)
703 :     ), "\n",
704 :     CGI::td(CGI::submit(-name=>'action', -value=>'Save as Default')),
705 :     )
706 :     );
707 : gage 1368
708 : sh002i 4490 ##############################################################################################################
709 : gage 1369
710 :     print CGI::end_form();
711 : gage 1368 return "";
712 :     }
713 :    
714 : gage 1369 ##############################################################################
715 :     # Utility methods
716 :     ##############################################################################
717 :    
718 : gage 1370 sub saveProblem {
719 :     my $self = shift;
720 :     my ($body, $probFileName)= @_;
721 :     local(*PROBLEM);
722 :     open (PROBLEM, ">$probFileName") ||
723 : toenail 2320 $self->addbadmessage(CGI::p("Could not open $probFileName for writing.
724 :     Check that the permissions for this problem are 660 (-rw-rw----)"));
725 : toenail 2084 print PROBLEM $body if -w $probFileName;
726 : gage 1370 close PROBLEM;
727 :     chmod 0660, "$probFileName" ||
728 : toenail 2320 $self->addbadmessage(CGI::p("CAN'T CHANGE PERMISSIONS ON FILE $probFileName"));
729 : gage 1370 }
730 :    
731 :     sub read_input_file {
732 :     my $self = shift;
733 :     my $filePath = shift;
734 :     my ($text, @text);
735 :     my $header = '';
736 :     my ($subject, $from, $replyTo);
737 :     local(*FILE);
738 : gage 1371 if (-e "$filePath" and -r "$filePath") {
739 : toenail 2320 open FILE, "$filePath" || do { $self->addbadmessage(CGI::p("Can't open $filePath")); return};
740 : gage 1371 while ($header !~ s/Message:\s*$//m and not eof(FILE)) {
741 : gage 1370 $header .= <FILE>;
742 :     }
743 :     $text = join( '', <FILE>);
744 :     $text =~ s/^\s*//; # remove initial white space if any.
745 :     $header =~ /^From:\s(.*)$/m;
746 :     $from = $1 or $from = $self->{defaultFrom};
747 :    
748 :     $header =~ /^Reply-To:\s(.*)$/m;
749 :     $replyTo = $1 or $replyTo = $self->{defaultReply};
750 :    
751 :     $header =~ /^Subject:\s(.*)$/m;
752 :     $subject = $1;
753 :    
754 :     } else {
755 :     $from = $self->{defaultFrom};
756 :     $replyTo = $self->{defaultReply};
757 : gage 1371 $text = (-e "$filePath") ? "FIXME file $filePath can't be read" :"FIXME file $filePath doesn't exist";
758 : dpvc 2388 $subject = $self->{defaultSubject};
759 : gage 1370 }
760 :     return ($from, $replyTo, $subject, \$text);
761 :     }
762 : gage 1371
763 :    
764 : gage 1397 sub get_message_file_names {
765 :     my $self = shift;
766 :     return $self->read_dir($self->{ce}->{courseDirs}->{email}, '\\.msg$');
767 : gage 1371 }
768 : gage 1397 sub get_merge_file_names {
769 :     my $self = shift;
770 : gage 1730 return 'None', $self->read_dir($self->{ce}->{courseDirs}->{scoring}, '\\.csv$'); #FIXME ? check that only readable files are listed.
771 : gage 1371 }
772 : gage 1372
773 : gage 3329 sub mail_message_to_recipients {
774 :     my $self = shift;
775 : sh002i 4596 my $r = $self->r;
776 : sh002i 4782 my $ce = $r->ce;
777 : gage 3329 my $subject = $self->{subject};
778 :     my $from = $self->{from};
779 :     my @recipients = @{$self->{ra_send_to}};
780 :     my $rh_merge_data = $self->{rh_merge_data};
781 :     my $merge_file = $self->{merge_file};
782 :     my $result_message = '';
783 :     my $failed_messages = 0;
784 : sh002i 4881
785 : gage 3329 foreach my $recipient (@recipients) {
786 :     # warn "FIXME sending email to $recipient";
787 :     my $error_messages = '';
788 :     my $ur = $self->{db}->getUser($recipient); #checked
789 :     unless ($ur) {
790 :     $error_messages .= "Record for user $recipient not found\n";
791 :     next;
792 :     }
793 :     unless ($ur->email_address) {
794 :     $error_messages .="User $recipient does not have an email address -- skipping\n";
795 :     next;
796 :     }
797 : sh002i 4596 my $msg = eval { $self->process_message($ur,$rh_merge_data) };
798 :     $error_messages .= "There were errors in processing user $recipient, merge file $merge_file. \n$@\n" if $@;
799 : gage 3329 my $mailer = Mail::Sender->new({
800 : sh002i 4782 from => $ce->{mail}{smtpSender},
801 :     fake_from => $from,
802 :     to => $ur->email_address,
803 :     smtp => $self->{smtpServer},
804 :     subject => $subject,
805 :     headers => "X-Remote-Host: ".$self->{remote_host},
806 : gage 3329 });
807 :     unless (ref $mailer) {
808 :     $error_messages .= "Failed to create a mailer for user $recipient: $Mail::Sender::Error\n";
809 :     next;
810 :     }
811 :     unless (ref $mailer->Open()) {
812 :     $error_messages .= "Failed to open the mailer for user $recipient: $Mail::Sender::Error\n";
813 :     next;
814 :     }
815 :     my $MAIL = $mailer->GetHandle() || ($error_messages .= "Couldn't get mailer handle \n");
816 :     print $MAIL $msg || ($error_messages .= "Couldn't print to $MAIL");
817 :     close $MAIL || ($error_messages .= "Couldn't close $MAIL");
818 :     #warn "FIXME mailed to $recipient: ", $ur->email_address, " from $from subject $subject Errors: $error_messages";
819 :     $failed_messages++ if $error_messages;
820 :     $result_message .= $error_messages;
821 :     }
822 :     my $courseName = $self->r->urlpath->arg("courseID");
823 :     my $number_of_recipients = scalar(@recipients) - $failed_messages;
824 :     $result_message = <<EndText.$result_message;
825 :    
826 :     A message with the subject line
827 :     $subject
828 :     has been sent to
829 :     $number_of_recipients recipient(s) in the class $courseName.
830 :     There were $failed_messages message(s) that could not be delivered.
831 :    
832 :     EndText
833 : gage 1397
834 : gage 3329 }
835 :     sub email_notification {
836 :     my $self = shift;
837 :     my $result_message = shift;
838 :     # find info on mailer and sender
839 :     # use the defaultFrom address.
840 :    
841 :     # find info on instructor recipient and message
842 :     my $subject="WeBWorK email sent";
843 :    
844 :     my $mailing_errors = "";
845 :     # open MAIL handle
846 :     my $mailer = Mail::Sender->new({
847 :     from => $self->{defaultFrom},
848 :     to => $self->{defaultFrom},
849 :     smtp => $self->{smtpServer},
850 :     subject => $subject,
851 : sh002i 4596 headers => "X-Remote-Host: ".$self->{remote_host},
852 : gage 3329 });
853 :     unless (ref $mailer) {
854 :     $mailing_errors .= "Failed to create a mailer: $Mail::Sender::Error";
855 :     return "";
856 :     }
857 :     unless (ref $mailer->Open()) {
858 :     $mailing_errors .= "Failed to open the mailer: $Mail::Sender::Error";
859 :     return "";
860 :     }
861 :     my $MAIL = $mailer->GetHandle();
862 :     # print message
863 :     print $MAIL $result_message;
864 :     # clean up
865 :     close $MAIL;
866 :    
867 :     warn "instructor message sent to ", $self->{defaultFrom};
868 :    
869 :     }
870 : gage 1372 sub getRecord {
871 :     my $self = shift;
872 :     my $line = shift;
873 :     my $delimiter = shift;
874 :     $delimiter = ',' unless defined($delimiter);
875 :    
876 :     # Takes a delimited line as a parameter and returns an
877 :     # array. Note that all white space is removed. If the
878 :     # last field is empty, the last element of the returned
879 :     # array is also empty (unlike what the perl split command
880 :     # would return). E.G. @lineArray=&getRecord(\$delimitedLine).
881 :    
882 :     my(@lineArray);
883 : gage 2001 $line.="${delimiter}___"; # add final field which must be non-empty
884 :     @lineArray = split(/\s*${delimiter}\s*/,$line); # split line into fields
885 : gage 1372 $lineArray[0] =~s/^\s*//; # remove white space from first element
886 : gage 2001 pop @lineArray; # remove the last artificial field
887 : gage 1372 @lineArray;
888 :     }
889 :    
890 :     sub process_message {
891 :     my $self = shift;
892 :     my $ur = shift;
893 :     my $rh_merge_data = shift;
894 : sh002i 4596 my $for_preview = shift;
895 : gage 1372 my $text = defined($self->{r_text}) ? ${ $self->{r_text} }:
896 : gage 1730 'FIXME no text was produced by initialization!!';
897 :     my $merge_file = ( defined($self->{merge_file}) ) ? $self->{merge_file} : 'None';
898 : sh002i 3690
899 :     my $status_name = $self->r->ce->status_abbrev_to_name($ur->status);
900 :     $status_name = $ur->status unless defined $status_name;
901 :    
902 : gage 1372 #user macros that can be used in the email message
903 :     my $SID = $ur->student_id;
904 :     my $FN = $ur->first_name;
905 :     my $LN = $ur->last_name;
906 :     my $SECTION = $ur->section;
907 :     my $RECITATION = $ur->recitation;
908 : sh002i 3690 my $STATUS = $status_name;
909 : gage 1372 my $EMAIL = $ur->email_address;
910 :     my $LOGIN = $ur->user_id;
911 : gage 1730
912 : gage 1372 # get record from merge file
913 :     # FIXME this is inefficient. The info should be cached
914 : gage 1373 my @COL = defined($rh_merge_data->{$SID}) ? @{$rh_merge_data->{$SID} } : ();
915 : sh002i 4596 if ($merge_file ne 'None' and not defined($rh_merge_data->{$SID}) and $for_preview) {
916 : toenail 2320 $self->addbadmessage(CGI::p("No merge data for student id:$SID; name:$FN $LN; login:$LOGIN"));
917 : gage 1730 }
918 : apizer 2779 unshift(@COL,""); ## this makes COL[1] the first column
919 : gage 1372 my $endCol = @COL;
920 :     # for safety, only evaluate special variables
921 : gage 1373 my $msg = $text;
922 : apizer 2787 $msg =~ s/\$SID/$SID/ge;
923 :     $msg =~ s/\$LN/$LN/ge;
924 :     $msg =~ s/\$FN/$FN/ge;
925 :     $msg =~ s/\$STATUS/$STATUS/ge;
926 :     $msg =~ s/\$SECTION/$SECTION/ge;
927 :     $msg =~ s/\$RECITATION/$RECITATION/ge;
928 :     $msg =~ s/\$EMAIL/$EMAIL/ge;
929 :     $msg =~ s/\$LOGIN/$LOGIN/ge;
930 :     if (defined($COL[1])) { # prevents extraneous error messages.
931 :     $msg =~ s/\$COL\[(\-?\d+)\]/$COL[$1]/ge
932 :     }
933 :     else { # prevents extraneous $COL's in email message
934 :     $msg =~ s/\$COL\[(\-?\d+)\]//g
935 :     }
936 :    
937 : gage 1373 $msg =~ s/\r//g;
938 : apizer 2779
939 : sh002i 4596 if ($for_preview) {
940 :     my @preview_COL = @COL;
941 :     shift @preview_COL; ## shift back for preview
942 : apizer 5727 my $preview_header = CGI::p('',data_format(1..($#COL)),"<br>", data_format2(@preview_COL)).
943 : sh002i 4596 CGI::h3( "This sample mail would be sent to $EMAIL");
944 :     return $msg, $preview_header;
945 :     } else {
946 :     return $msg;
947 :     }
948 : gage 1372 }
949 : gage 1948
950 :    
951 : sh002i 3690 # Ý sub data_format {
952 : gage 1948 #
953 : sh002i 3690 # Ý Ý Ý Ý Ýmap {$_ =~s/\s/\./g;$_} Ý Ý map {sprintf('%-8.8s',$_);} Ý@_;
954 : gage 1372 sub data_format {
955 : gage 1948 map {"COL[$_]".'&nbsp;'x(3-length($_));} @_; # problems if $_ has length bigger than 4
956 : gage 1372 }
957 : gage 1947 sub data_format2 {
958 : gage 1948 map {$_ =~s/\s/&nbsp;/g;$_} map {sprintf('%-8.8s',$_);} @_;
959 : gage 1947 }
960 : gage 1368 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9