/[drupal]/contributions/modules/mailout/mailout.module
ViewVC logotype

Contents of /contributions/modules/mailout/mailout.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.1 - (show annotations) (download) (as text)
Mon Apr 14 04:10:43 2008 UTC (19 months, 2 weeks ago) by kingmoore
Branch: MAIN
CVS Tags: DRUPAL-5--0-0, HEAD
Branch point for: DRUPAL-5
File MIME type: text/x-php
initial malout module commit
1 <?php
2 // $Id$
3
4 /**
5 * @file mailout.module
6 * mailout module to send email newsletters to an uploaded CSV list
7 */
8
9 /**
10 * Implementation of hook_menu
11 */
12 function mailout_menu($may_cache){
13 $items = array();
14 if ($may_cache) {
15 $items[] = array(
16 'path' => 'admin/content/mailout',
17 'callback' => 'mailout_send',
18 'title' => t('Mailout'),
19 'access' => user_access('send mailout'),
20 'type' => MENU_ITEM_GROUPING
21 );
22 $items[] = array('path' => 'admin/content/mailout/send',
23 'title' => t('Send Mailout'),
24 'callback' => 'mailout_send',
25 'type' => MENU_NORMAL_ITEM,
26 'access' => user_access('send mailout'),
27 'weight' => 1);
28 $items[] = array(
29 'path' => 'mailout/stats_update',
30 'callback' => 'mailout_stats_update',
31 'access' => TRUE,
32 'type' => MENU_CALLBACK
33 );
34 $items[] = array(
35 'path' => 'admin/content/mailout/stats',
36 'title' => t('Mailout Stats'),
37 'callback' => 'mailout_stats',
38 'access' => user_access('view mailout stats'),
39 'type' => MENU_NORMAL_ITEM
40 );
41 $items[] = array(
42 'path' => 'admin/content/mailout/run_cron',
43 'callback' => 'mailout_run_cron',
44 'access' => TRUE,
45 'type' => MENU_CALLBACK
46 );
47 }
48 return $items;
49 }
50
51 /**
52 * Implementation of hook_node_info()
53 */
54 function mailout_node_info() {
55
56 return array(
57 'mailout_template' => array(
58 'name' => t('Mailout Template'),
59 'module' => 'mailout_template',
60 'description' => t("A Mailout Template type defines an email template that can be used to send a mailout to an address list."),
61 'has_title' => TRUE,
62 'title_label' => 'Template Name',
63 'has_body' => TRUE,
64 'body_label' => 'Mail Body',
65 )
66 );
67 }
68
69 /**
70 * Implemenation of hook_perm()
71 */
72 function mailout_perm(){
73 return array('administer mailout_templates', 'administer address_lists', 'send mailout', 'view mailout stats');
74 }
75
76 /**
77 * Implementation of hook_access()
78 * for content type mailout_template
79 */
80 function mailout_template_access($op, $node){
81
82 switch ($op) {
83 case "create":
84 case "delete":
85 case "update":
86 case "view":
87 return user_access('administer mailout_templates');
88 break;
89 }
90
91 }
92
93 /**
94 * implementation of hook_form()
95 * for content type mailout_template
96 */
97 function mailout_template_form($node){
98 $type = node_get_types('type', $node);
99
100 $form['title'] = array(
101 '#type'=> 'textfield',
102 '#title' => check_plain($type->title_label),
103 '#default_value' => $node->title,
104 '#required' => TRUE,
105 );
106 $form['email_from'] = array(
107 '#type'=> 'textfield',
108 '#title' => t('Mail From Address'),
109 '#default_value' => $node->email_from,
110 '#required' => FALSE,
111 );
112 $form['email_subject'] = array(
113 '#type'=> 'textfield',
114 '#title' => t('Mail Subject'),
115 '#default_value' => $node->email_subject,
116 '#required' => FALSE,
117 );
118 $form['body'] = array(
119 '#type' => 'textarea',
120 '#title' => check_plain($type->body_label),
121 '#rows' => 10,
122 '#default_value' => $node->body,
123 '#required' => FALSE,
124 );
125 $form['email_header'] = array(
126 '#type' => 'textarea',
127 '#title' => t('Mail Header'),
128 '#rows' => 10,
129 '#default_value' => $node->email_header,
130 '#required' => FALSE,
131 );
132 $form['email_footer'] = array(
133 '#type' => 'textarea',
134 '#title' => t('Mail Footer'),
135 '#rows' => 10,
136 '#default_value' => $node->email_footer,
137 '#required' => FALSE,
138 );
139
140 return $form;
141 }
142
143 /**
144 * knows how to take form values (file, addresses field etc.)
145 * and return all addresses back in a | delimited list
146 *
147 * @param unknown_type $form_values
148 * @return unknown
149 */
150 function all_addresses($form_values, $delete_file = FALSE){
151
152 //initialize arrays to hold addresses before merge
153 $address_list = array();
154 $form_address_list = array();
155 $role_address_list = array();
156 $list_address_list = array();
157 $file_address_list = array();
158
159 //add addresses from specified roles
160 if(count($form_values['address_roles'])){
161
162 //if all users, get all user email addresses
163 if($form_values['address_roles'][2] != 0){
164 $sql = "SELECT distinct mail FROM {users}";
165 $result = db_query($sql);
166 }
167 //else get users for specified roles
168 else {
169 $sql = "SELECT distinct mail FROM {users} JOIN {users_roles} on users.uid = users_roles.uid WHERE users_roles.rid IN (%s)";
170 $result = db_query($sql, implode(",",$form_values['address_roles']));
171 }
172
173 while($row = db_fetch_array($result)){
174 array_push($role_address_list, $row['mail']);
175 }
176
177 //merge addresses
178 $address_list = array_merge($address_list, $role_address_list);
179 }
180
181
182 //add addresses from specified mailout lists
183 if(count($form_values['address_lists'])){
184
185 $sql = "SELECT distinct email FROM {mailout_list_address} WHERE list_id IN (%s)";
186 $result = db_query($sql, implode(",", $form_values['address_lists']));
187 while($row = db_fetch_array($result)){
188 array_push($list_address_list, $row['email']);
189 }
190
191 //merge addresses
192 $address_list = array_merge($address_list, $list_address_list);
193 }
194
195 //array to hold allowed file extensions
196 $allowed_extensions = array('CSV');
197
198 // Check for a new file upload.
199 if ($file = file_check_upload('address_file')) {
200
201 //get file extension
202 $file_extension = array_pop(explode('.',$file->filename));
203
204 //make sure uploaded file is allowed extension
205 if(in_array(strtoupper($file_extension), $allowed_extensions)){
206
207 //open file and add all addresses to $address_list array
208 $handle = fopen($file->filepath, "r");
209 $message_count = 0;
210 while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
211 array_push($file_address_list, $data[0]);
212 }
213 fclose($handle);
214
215 //delete file
216 if($delete_file)
217 file_delete($file->filepath);
218
219 }
220 else {
221 form_set_error('file', t('Uploaded file must be a .CSV file.'));
222 }
223 }
224
225 //merge addresses
226 $address_list = array_merge($address_list, $file_address_list);
227
228 //get addresses from form field into array
229 if($form_values['address_field']){
230
231 //do gmail replacement
232 $pattern = "#\"(.*?)\" <(.*?)>#";
233 $replacement = '$2';
234 $form_addresses = preg_replace($pattern, $replacement, $form_values['address_field']);
235
236 //do splitting
237 $form_addresses = str_replace("\r", "", $form_addresses);
238 $form_addresses = str_replace("\n", "|", $form_addresses);
239 $form_addresses = str_replace(" ", "|", $form_addresses);
240 $form_addresses = str_replace(",", "|", $form_addresses);
241 $form_addresses = str_replace(";", "|", $form_addresses);
242 $form_address_list = explode("|", $form_addresses);
243 }
244
245 //merge addresses
246 $address_list = array_merge($address_list, $form_address_list);
247
248 //remove blanks
249 foreach($address_list as $key => $value) {
250 if($value == "" || $value == " " || is_null($value)) {
251 unset($address_list[$key]);
252 }
253 }
254
255 if(count($address_list))
256 return implode("|", array_unique($address_list));
257 else
258 return 0;
259 }
260
261 /**
262 * implementation of hook_form_alter
263 * sets promote to front page value to 0 for mailout_template
264 *
265 * @param unknown_type $form_id
266 * @param unknown_type $form
267 */
268 function mailout_form_alter($form_id, &$form){
269
270 //please don't promote my types to front page
271 if($form['type']['#value'] == 'mailout_template' || $form['type']['#value'] == 'address_list'){
272 $form['options']['promote']['#default_value'] = 0;
273 }
274
275 }
276
277 /**
278 * mailout_stats knows how to display statistics
279 * about mailouts that have been sent
280 *
281 */
282 function mailout_stats(){
283
284 $header_row = array('ID', 'Subject', 'Messages', 'Sent', 'Read', 'Timestamp');
285 //get mailout stats
286 $result = db_query('SELECT l.id, l.email_subject, count(ml.id) as num_mails, sum(case when ml.sent = 1 then 1 else 0 end) as sent, sum(case when ml.opened = 1 then 1 else 0 end) as opened, l.id, l.timestamp as timestamp FROM {mailout_log} as l join {mailout_message_log} as ml on ml.mailout_log_id = l.id GROUP BY l.id ORDER BY timestamp desc');
287
288 $table_rows = array();
289 while ($row = db_fetch_array($result)) {
290 $table_rows[] = $row;
291 }
292
293 $output = theme('mailout_stats', $table_rows, $header_row);
294
295 return $output;
296 }
297
298 /**
299 * mailout_stats_update() gets called when a user opens an email sent via
300 * a mailout. The mailout includes 1x1 image whose source
301 * maps to this namespace and in turn updates the mailout_message_log
302 * table, setting opened = 1 where the email address and mailout
303 * id equal the ones requested in the URL like
304 * /mailout/stats_update/email@address.com/3/image.jpg
305 *
306 */
307 function mailout_stats_update(){
308
309 if(($email_address = arg(2)) && ($mailout = arg(3))){
310 db_query("UPDATE {mailout_message_log} SET opened = 1 WHERE mailout_log_id = %d AND email_to = '%s'",
311 $mailout, $email_address);
312
313 return "$email_address marked as open for mailout: $mailout.";
314 }
315
316 return "Arguments not valid";
317 }
318
319 /**
320 * Implementation of hook_cron()
321 * send emails that need to be sent
322 */
323 function mailout_cron(){
324
325 global $base_url;
326
327 //get mailout stats
328 $result = db_query('SELECT ml.id, l.id as log_id, ml.email_to, l.email_headers, l.email_subject, l.email_body, l.email_from FROM mailout_message_log AS ml JOIN mailout_log AS l ON ml.mailout_log_id = l.id WHERE sent = 0 AND l.scheduled < %d', time());
329
330 while ($data = db_fetch_array($result)) {
331
332 //unserialize the headers
333 $email_headers = unserialize($data['email_headers']);
334
335 //add custom tracking image to each email
336 $final_email_body = $data['email_body']."<img src=\"$base_url/mailout/stats_update/$data[email_to]/$data[log_id]/image.jpg\" width=\"0\" height=\"0\">";
337
338 //send mail
339 drupal_mail($data['id'], $data['email_to'], $data['email_subject'], $final_email_body, $data['email_from'], $email_headers);
340 db_query('UPDATE {mailout_message_log} SET sent = 1 WHERE id = %d', $data['id']);
341 }
342
343 }
344
345
346 /**
347 * Menu callback: run cron manually.
348 */
349 function mailout_run_cron() {
350 // Run cron manually
351 if (drupal_cron_run()) {
352 drupal_set_message(t('Cron ran successfully.'));
353 }
354 else {
355 drupal_set_message(t('Cron run failed.'));
356 }
357
358 drupal_goto('admin/content/mailout/stats');
359 }
360 /**
361 * callback for mailout/send
362 *
363 * @return $form
364 */
365 function mailout_send(){
366 return drupal_get_form('mailout_form');
367 }
368
369 /**
370 * multi step form for sending a mailout
371 *
372 * @param unknown_type $form_values
373 * @return $form
374 */
375 function mailout_form($form_values = NULL){
376
377 if(!isset($form_values)){
378 $step = 1;
379
380 //session variable to hold email addresses for all_addresses
381 $_SESSION['mailout_all_addresses'] = "";
382 }
383 else {
384 $step = $form_values['step'] + 1;
385 }
386
387 if($step == 2 && !$_SESSION['mailout_all_addresses']){
388 $_SESSION['mailout_all_addresses'] = all_addresses($form_values, TRUE);
389 }
390
391 $form['#attributes'] = array('enctype' => "multipart/form-data");
392
393 //hidden field to hold step
394 $form['step'] = array(
395 '#type' => 'hidden',
396 '#value' => $step,
397 );
398
399 switch ($step) {
400 case 1:
401 //get all mailout templates
402 $mailout_templates = array('0' => " - No Template - ");
403 $result = db_query('SELECT n.nid, n.title FROM {mailout_template} as m INNER JOIN node as n ON m.nid = n.nid ORDER BY n.title');
404 while ($row = db_fetch_array($result)) {
405 $nid = $row['nid'];
406 $title = $row['title'];
407
408 $mailout_templates[$nid] = $title;
409 }
410
411 $form['mailout_template_id'] = array(
412 '#type' => 'select',
413 '#title' => t('Mailout Template'),
414 '#options' => $mailout_templates,
415 '#required' => FALSE,
416 '#description' => t('Select a Mailout Template for the Mailout. <a href="../../../node/add/mailout-template">Create New Template</a>'),
417 );
418
419 $form['schedule_date'] = array(
420 '#type' => 'date',
421 '#title' => t('Scheduled Date'),
422 '#description' => t("Schedule mailout to be sent at some date in the future"),
423 '#required' => FALSE,
424 );
425
426 $form['address_field'] = array(
427 '#type' => 'textarea',
428 '#title' => t('Email Addresses'),
429 '#rows' => 4,
430 '#default_value' => $mailout_node->body,
431 '#description' => t("Dump email addresses here. Comma delimited, space delimited, or one per line."),
432 '#required' => FALSE,
433 );
434
435 //get all roles except anonymous and authenticated, set our own All Users
436 //in place of authenticated
437 $role_array = array('2' => 'All Users');
438 $sql = "SELECT rid, name FROM {role} WHERE rid > 2 ORDER BY name, rid";
439 $result = db_query($sql);
440 while($row = db_fetch_array($result)){
441 $role_id = $row['rid'];
442 $role_name = $row['name'];
443 $role_array[$role_id] = $role_name;
444 }
445
446 //this field allows sending of mailout to all users of selected role or all users
447 $form['address_roles'] = array(
448 '#type' => 'checkboxes',
449 '#title' => t('Send to Role(s)'),
450 '#options' => $role_array,
451 '#description' => t("Send mailout to selected user roles"),
452 '#required' => FALSE,
453 );
454
455 //build options list of all mailout lists
456 $list_array = array();
457 $sql = "SELECT id, list_name FROM {mailout_list} ORDER BY list_name";
458 $result = db_query($sql);
459 while($row = db_fetch_array($result)){
460 $list_id = $row['id'];
461 $list_name = $row['list_name'];
462 $list_array[$list_id] = $list_name;
463 }
464
465 //this field allows sending of mailout to mailout lists
466 if(count($list_array)){
467 $form['address_lists'] = array(
468 '#type' => 'checkboxes',
469 '#title' => t('Send to Mailout List(s)'),
470 '#options' => $list_array,
471 '#description' => t("Send mailout to one or more mailout lists"),
472 '#required' => FALSE,
473 );
474 }
475
476 // file upload field
477 $form['address_file'] = array(
478 '#type' => 'file',
479 '#title' => t('CSV Address List'),
480 '#size' => 40,
481 '#default_value' => '',
482 '#description' => t("Please browse and upload a .CSV email list"),
483 '#required' => FALSE,
484 );
485
486 // This part is important!
487 $form['#multistep'] = TRUE;
488 $form['#redirect'] = FALSE;
489
490 $form['submit'] = array(
491 '#type' => 'submit',
492 '#value' => t('Submit'),
493 );
494 break;
495
496 case 2:
497
498 $mailout_node = node_load($form_values['mailout_template_id']);
499
500 $form['all_addresses'] = array(
501 '#type' => 'hidden',
502 '#value' => $_SESSION['mailout_all_addresses'],
503 );
504
505 $form['scheduled_timestamp'] = array(
506 '#type' => 'hidden',
507 '#value' => mktime(0,0,0,$form_values['schedule_date']['month'],$form_values['schedule_date']['day'],$form_values['schedule_date']['year']),
508 );
509
510 $form['mailout_template_id'] = array(
511 '#type' => 'hidden',
512 '#value' => $form_values['mailout_template_id'],
513 );
514 $form['email_from'] = array(
515 '#type'=> 'textfield',
516 '#title' => t('Mailout From Address'),
517 '#default_value' => $mailout_node->email_from,
518 '#required' => TRUE,
519 );
520 $form['email_subject'] = array(
521 '#type'=> 'textfield',
522 '#title' => t('Mailout Subject'),
523 '#default_value' => $mailout_node->email_subject,
524 '#required' => TRUE,
525 );
526 $form['body'] = array(
527 '#type' => 'textarea',
528 '#title' => t('Mailout Body'),
529 '#rows' => 10,
530 '#default_value' => $mailout_node->body,
531 '#required' => FALSE,
532 );
533
534 // This part is important!
535 $form['#multistep'] = TRUE;
536 $form['#redirect'] = FALSE;
537
538 $form['submit'] = array(
539 '#type' => 'submit',
540 '#value' => t('Send Mailout'),
541 );
542 //subject / from / body fields
543 break;
544 }
545
546 return $form;
547
548 }
549
550 /**
551 * implementation of hook_validate for mailout_form
552 */
553 function mailout_form_validate($form_id, $form_values){
554
555 if($form_values['step'] == 1){
556 $all_addresses = all_addresses($form_values);
557
558 if($all_addresses){
559 $num_addresses = count(explode("|",$all_addresses));
560 drupal_set_message("Mailout will be sent to ".$num_addresses." recipients.", "status");
561 }
562 else {
563 form_set_error("address_field", "No email addresses specified through any method.");
564 }
565 }
566 }
567
568 /**
569 * implementation of hook_submit()
570 * used for implementation of multi step form
571 *
572 * @param unknown_type $form_id
573 * @param unknown_type $form_values
574 */
575 function mailout_form_submit($form_id, $form_values) {
576
577 $final_step = 2;
578
579 //if first step, get emails and file emails together into hidden field
580 if($form_values['step'] == 1){
581
582
583 } else if ($form_values['step'] == $final_step) {
584
585 //load address list
586 $address_list = explode("|", $form_values['all_addresses']);
587
588 //load address list
589 $mailout_template = node_load($form_values['mailout_template_id']);
590
591 //build headers and serialize
592 $headers['Content-Type'] = " text/html; charset=utf-8";
593 $headers["MIME-Version"] = '1.0';
594 $headers["Content-Transfer-Encoding"] = '8bit';
595 $serialized_headers = serialize($headers);
596
597 //build message & include stat track line
598 $email_body = $mailout_template->email_header;
599 $email_body .= $form_values['body'];
600 $email_body .= $mailout_template->email_footer;
601
602 //insert db record for send
603 db_query("INSERT INTO {mailout_log} (mailout_template_id, email_headers, email_subject, email_from, email_body, scheduled) VALUES (%d, '%s', '%s', '%s', '%s', %d)", $form_values['mailout_template_id'], $serialized_headers, $form_values['email_subject'], $form_values['email_from'], $email_body, $form_values['scheduled_timestamp']);
604
605 //get log_id just inserted
606 $result = db_query('SELECT MAX(id) as max_id FROM {mailout_log}');
607 $row = db_fetch_array($result);
608 $max_id = $row['max_id'];
609
610 $message_count = 0;
611 foreach ($address_list as $key=>$value) {
612
613 //insert db record for each insert
614 db_query("INSERT INTO {mailout_message_log} (mailout_log_id, email_to) VALUES (%d, '%s')", $max_id, $value);
615
616 $message_count++;
617
618 }
619
620 //output details
621 drupal_set_message("Mailout scheduled to send to $message_count addresses on next cron run.<br>".l('Click here to run cron now', 'admin/content/mailout/run_cron'));
622 }
623 }
624
625 /**
626 * Implementation of hook_insert()
627 * for content type mailout_template
628 */
629 function mailout_template_insert($node){
630 db_query("INSERT INTO {mailout_template} (nid,vid,email_header,email_footer,email_from,email_subject) VALUES (%d, %d, '%s', '%s', '%s', '%s')",
631 $node->nid, $node->vid, $node->email_header, $node->email_footer, $node->email_from, $node->email_subject);
632 }
633
634 /**
635 * Implementation of hook_update()
636 * for content type mailout_template
637 */
638 function mailout_template_update($node){
639 if($node->revision){
640 mailout_template_insert($node);
641 }
642 else {
643 db_query("UPDATE {mailout_template} SET email_header = '%s', email_footer = '%s', email_from = '%s', email_subject = '%s' WHERE vid = %d",
644 $node->email_header, $node->email_footer, $node->email_from, $node->email_subject, $node->vid);
645 }
646 }
647
648 /**
649 * Implementation of hook_delete()
650 * for content type mailout_template
651 */
652 function mailout_template_delete($node){
653 db_query("DELETE FROM {mailout_template} WHERE nid = %d", $node->nid);
654 }
655
656 /**
657 * Implementation of hook_load()
658 * for content type mailout_template
659 */
660 function mailout_template_load($node){
661 $additions = db_fetch_object(db_query('SELECT email_header, email_footer, email_from, email_subject FROM {mailout_template} WHERE vid = %d', $node->vid));
662 return $additions;
663 }
664
665 /**
666 * Implementation of hook_block() to provide mailout list signup blocks
667 *
668 */
669 function mailout_block($op = 'list', $delta = 0, $edit = array()) {
670
671 //return list of blocks
672 if ($op == 'list') {
673
674 //get all mail lists
675 $sql = "SELECT id, list_name FROM mailout_list ORDER BY list_name";
676 $result = db_query($sql);
677
678 while($row = db_fetch_array($result)) {
679 $list_id = $row['id'];
680 $list_name = $row['list_name'];
681
682 $blocks[$list_id] = array('info' => t('Subscribe to '.$list_name));
683 }
684
685 return $blocks;
686 }
687 //return block view content
688 else if ($op == 'view') {
689
690 $sql = "SELECT id, list_name FROM mailout_list WHERE id = %d ORDER BY list_name";
691 $result = db_query($sql, $delta);
692 $row = db_fetch_array($result);
693 $list_name = $row['list_name'];
694 $list_id = $row['id'];
695
696 $block = array('subject' => t('Subscribe to '.$list_name), 'content' => mailout_display_subscribe_block($list_id, $list_name));
697
698 return $block;
699
700 }
701 }
702
703 function mailout_display_subscribe_block($list_id, $list_name){
704 return drupal_get_form('subscribe_box_form', $list_id, $list_name);
705 }
706
707 function subscribe_box_form($list_id, $list_name){
708
709 $form['list_id'] = array(
710 '#type' => 'hidden',
711 '#value' => $list_id,
712 );
713 $form['email'] = array(
714 '#type'=> 'textfield',
715 '#title' => t('Email'),
716 '#size' => 20,
717 '#default_value' => '',
718 '#required' => TRUE,
719 );
720 $form['name'] = array(
721 '#type'=> 'textfield',
722 '#title' => t('Name'),
723 '#size' => 20,
724 '#default_value' => '',
725 '#required' => TRUE,
726 );
727 $form['submit'] = array(
728 '#type' => 'submit',
729 '#value' => t('Subscribe'),
730 );
731
732 return $form;
733
734 }
735
736 function subscribe_box_form_submit($form_id, $form_values){
737 $sql = "INSERT INTO mailout_list_addresses(email,list_id) VALUES ('%s', %d)";
738 $result = db_query($sql, $form_values['email'], $form_values['list_id']);
739 }
740
741 function theme_subscribe_box_form($form, $list_id, $list_name){
742
743 $return_data = "<div id=\"subscribe_box_form_$list_id\" class=\"subscribe_box_form\">";
744 $return_data .= "<div class=\"subscribe_box_form_title\">Subscribe to $list_name</div>";
745 //$return_data .= drupal_get_form('subscribe_box_form', $list_id, $list_name);
746 $return_data .= drupal_render($form);
747 $return_data .= "</div>";
748
749 return $return_data;
750 }
751
752 /**
753 * Theme function to theme the stats page/table
754 *
755 * @param $rows
756 * table rows to theme
757 * @param $headers
758 * table headers to theme
759 */
760 function theme_mailout_stats($rows, $headers){
761 return theme_table($headers, $rows);
762 }

  ViewVC Help
Powered by ViewVC 1.1.2