View Javadoc
1   /*
2    * Copyright (c) 2002-2021, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.newsletter.service;
35  
36  import fr.paris.lutece.plugins.newsletter.business.NewsLetter;
37  import fr.paris.lutece.plugins.newsletter.business.NewsLetterHome;
38  import fr.paris.lutece.plugins.newsletter.business.Subscriber;
39  import fr.paris.lutece.plugins.newsletter.business.SubscriberHome;
40  import fr.paris.lutece.plugins.newsletter.business.topic.NewsletterTopic;
41  import fr.paris.lutece.plugins.newsletter.business.topic.NewsletterTopicHome;
42  import fr.paris.lutece.plugins.newsletter.service.topic.INewsletterTopicService;
43  import fr.paris.lutece.plugins.newsletter.service.topic.NewsletterTopicService;
44  import fr.paris.lutece.plugins.newsletter.util.NewsLetterConstants;
45  import fr.paris.lutece.plugins.newsletter.util.NewsletterUtils;
46  import fr.paris.lutece.plugins.newsletter.business.NewsLetterTemplateHome;
47  import fr.paris.lutece.portal.business.user.AdminUser;
48  import fr.paris.lutece.portal.service.mail.MailService;
49  import fr.paris.lutece.portal.service.plugin.Plugin;
50  import fr.paris.lutece.portal.service.plugin.PluginService;
51  import fr.paris.lutece.portal.service.spring.SpringContextService;
52  import fr.paris.lutece.portal.service.template.AppTemplateService;
53  import fr.paris.lutece.portal.service.util.AppLogService;
54  import fr.paris.lutece.portal.service.util.AppPathService;
55  import fr.paris.lutece.portal.service.util.AppPropertiesService;
56  import fr.paris.lutece.portal.service.util.CryptoService;
57  import fr.paris.lutece.util.html.HtmlTemplate;
58  import fr.paris.lutece.util.mail.UrlAttachment;
59  import fr.paris.lutece.util.string.StringUtil;
60  
61  import java.io.BufferedWriter;
62  import java.io.ByteArrayOutputStream;
63  import java.io.IOException;
64  import java.io.OutputStreamWriter;
65  import java.io.Serializable;
66  import java.io.UnsupportedEncodingException;
67  import java.util.ArrayList;
68  import java.util.Collection;
69  import java.util.Collections;
70  import java.util.HashMap;
71  import java.util.List;
72  import java.util.Locale;
73  import java.util.Map;
74  
75  import org.apache.commons.lang3.StringUtils;
76  
77  import au.com.bytecode.opencsv.CSVWriter;
78  
79  /**
80   * The newsletter service
81   * 
82   */
83  public class NewsletterService implements Serializable
84  {
85      /**
86       * Name of the bean of this service
87       */
88      public static final String BEAN_NAME = "newsletter.newsletterService";
89  
90      /**
91       * Serial version UID
92       */
93      private static final long serialVersionUID = 1644159439192572037L;
94  
95      // PROPERTIES
96      private static final String PROPERTY_ABSOLUTE_URL_MAIL = "newsletter.absolute.mail.url";
97      private static final String PROPERTY_PATH_IMAGE_NEWSLETTER_TEMPLATE = "newsletter.path.image.newsletter.template";
98      private static final String PROPERTY_NO_SECURED_IMG_FOLDER = "newsletter.nosecured.img.folder.name";
99      private static final String PROPERTY_WEBAPP_PATH = "newsletter.nosecured.webapp.path";
100     private static final String PROPERTY_WEBAPP_URL = "newsletter.nosecured.webapp.url";
101     private static final String PROPERTY_NO_SECURED_IMG_OPTION = "newsletter.nosecured.img.option";
102     private static final String PROPERTY_UNSUBSCRIBE_KEY_ENCRYPTION_ALGORITHM = "newsletter.unsubscribe.key.encryptionAlgorithm";
103 
104     private NewsletterTopicService _newsletterTopicService;
105 
106     /**
107      * Returns the instance of the singleton
108      * 
109      * @return The instance of the singleton
110      */
111     public static NewsletterService getService( )
112     {
113         return SpringContextService.getBean( BEAN_NAME );
114     }
115 
116     /**
117      * Send the newsletter to a list of subscribers
118      * 
119      * @param newsletter
120      *            The newsletter to send
121      * @param strObject
122      *            The email object
123      * @param strBaseUrl
124      *            The baseUrl (can be prod url)
125      * @param templateNewsletter
126      *            The generated template
127      * @param listSubscribers
128      *            The list of subscribers (date and id can be null, only email is used)
129      */
130     public void sendMail( NewsLetter newsletter, String strObject, String strBaseUrl, HtmlTemplate templateNewsletter, Collection<Subscriber> listSubscribers )
131     {
132         List<UrlAttachment> urlAttachments = null;
133         HtmlTemplate templateNewsletterToUse = templateNewsletter;
134         if ( isMhtmlActivated( ) )
135         {
136             // we use absolute urls if there is no preproduction process
137             boolean useAbsoluteUrl = isAbsoluteUrl( );
138             String strTemplate = templateNewsletterToUse.getHtml( );
139             strTemplate = StringUtil.substitute( strTemplate, strBaseUrl, NewsLetterConstants.WEBAPP_PATH_FOR_LINKSERVICE );
140             urlAttachments = MailService.getUrlAttachmentList( strTemplate, strBaseUrl, useAbsoluteUrl );
141 
142             // all images, css urls are relative
143             if ( !useAbsoluteUrl )
144             {
145                 templateNewsletterToUse.substitute( strBaseUrl, strBaseUrl.replaceFirst( "https?://[^/]+/", "/" ) );
146             }
147             else
148             {
149                 String strContent = NewsletterUtils.rewriteUrls( templateNewsletterToUse.getHtml( ), strBaseUrl );
150                 templateNewsletterToUse = new HtmlTemplate( strContent );
151             }
152         }
153 
154         for ( Subscriber subscriber : listSubscribers )
155         {
156             HtmlTemplate t = new HtmlTemplate( templateNewsletterToUse );
157 
158             t.substitute( NewsLetterConstants.MARK_SUBSCRIBER_EMAIL_EACH, subscriber.getEmail( ) );
159             if ( Boolean.parseBoolean( newsletter.getUnsubscribe( ) ) )
160             {
161                 String strUnsubscribeKey = getUnsubscriptionKey( subscriber.getEmail( ) );
162                 t.substitute( NewsLetterConstants.MARK_UNSUBSCRIBE_KEY_EACH, strUnsubscribeKey );
163             }
164 
165             String strNewsLetterCode = t.getHtml( );
166 
167             if ( ( urlAttachments == null ) || ( urlAttachments.size( ) == 0 ) )
168             {
169                 MailService.sendMailHtml( subscriber.getEmail( ), newsletter.getNewsletterSenderName( ), newsletter.getNewsletterSenderMail( ), strObject,
170                         strNewsLetterCode );
171             }
172             else
173             {
174                 MailService.sendMailMultipartHtml( subscriber.getEmail( ), newsletter.getNewsletterSenderName( ), newsletter.getNewsletterSenderMail( ),
175                         strObject, strNewsLetterCode, urlAttachments );
176             }
177         }
178     }
179 
180     /**
181      * Check the property in property file to know if url must be absolutes or relatives
182      * 
183      * @return true if absolute or false else
184      */
185     public boolean isAbsoluteUrl( )
186     {
187         boolean useAbsoluteUrl = false;
188         String strUseAbsoluteUrl = AppPropertiesService.getProperty( PROPERTY_ABSOLUTE_URL_MAIL );
189 
190         if ( ( strUseAbsoluteUrl != null ) && strUseAbsoluteUrl.equalsIgnoreCase( Boolean.TRUE.toString( ) ) )
191         {
192             useAbsoluteUrl = true;
193         }
194 
195         return useAbsoluteUrl;
196     }
197 
198     /**
199      * Determine if mails must be sent in MHTML
200      * 
201      * @return true whether MHTML is needed
202      */
203     public boolean isMhtmlActivated( )
204     {
205         String strProperty = AppPropertiesService.getProperty( NewsLetterConstants.PROPERTY_MAIL_MULTIPART );
206         return ( strProperty != null ) && Boolean.valueOf( strProperty ).booleanValue( );
207     }
208 
209     /**
210      * Fetches the list of subscribers on a specific newsletter
211      * 
212      * @param nNewsletterId
213      *            The id of the newsletter
214      * @return The byte representation of the list of subscribers
215      */
216     public byte [ ] getSubscribersCsvExport( int nNewsletterId )
217     {
218         byte [ ] byteSubscribersList = null;
219 
220         try
221         {
222             ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream( );
223             CSVWriter writer = new CSVWriter( new BufferedWriter( new OutputStreamWriter( byteArrayStream, "UTF-8" ) ) );
224             Collection<Subscriber> listSubscriber = SubscriberHome.findSubscribers( nNewsletterId, getPlugin( ) );
225 
226             for ( Subscriber subscriber : listSubscriber )
227             {
228                 String [ ] arraySubscriber = new String [ 3];
229                 arraySubscriber [0] = Integer.toString( subscriber.getId( ) );
230                 arraySubscriber [1] = subscriber.getEmail( );
231                 arraySubscriber [2] = subscriber.getDateSubscription( ).toString( );
232                 writer.writeNext( arraySubscriber );
233             }
234 
235             writer.close( );
236             byteSubscribersList = byteArrayStream.toByteArray( );
237         }
238         catch( UnsupportedEncodingException e )
239         {
240             AppLogService.error( e );
241         }
242         catch( IOException e )
243         {
244             AppLogService.error( e );
245         }
246 
247         return byteSubscribersList;
248     }
249 
250     /**
251      * Remove a known suscriber from a newsletter
252      * 
253      * @param subscriber
254      *            the subscriber to remove
255      * @param nNewsletterId
256      *            the newsletter id from which to remove the subscriber
257      * @param plugin
258      *            The plugin object
259      */
260     public void removeSubscriberFromNewsletter( Subscriber subscriber, int nNewsletterId, Plugin plugin )
261     {
262         /* checks newsletter exist in database */
263         NewsLetter newsletter = NewsLetterHome.findByPrimaryKey( nNewsletterId, plugin );
264 
265         if ( ( subscriber != null ) && ( newsletter != null ) )
266         {
267             int nSubscriberId = subscriber.getId( );
268 
269             /* checks if the subscriber identified is registered */
270             if ( NewsLetterHome.findRegistration( nNewsletterId, nSubscriberId, plugin ) )
271             {
272                 /* unregistration */
273                 NewsLetterHome.removeSubscriber( nNewsletterId, nSubscriberId, plugin );
274             }
275 
276             /*
277              * if the subscriber is not registered to an other newsletter, his account is deleted
278              */
279             if ( SubscriberHome.findNewsLetters( nSubscriberId, plugin ) == 0 )
280             {
281                 SubscriberHome.remove( nSubscriberId, plugin );
282             }
283         }
284     }
285 
286     /**
287      * Generate the html code of the newsletter according to the document and newsletter templates
288      * 
289      * @param newsletter
290      *            the newsletter to generate the HTML of
291      * @param nTemplateNewsLetterId
292      *            the newsletter template id
293      * @param strBaseUrl
294      *            The base url of the portal
295      * @param user
296      *            the current user
297      * @param locale
298      *            The locale
299      * @return the html code for the newsletter content of null if no template available
300      */
301     public String generateNewsletterHtmlCode( NewsLetter newsletter, int nTemplateNewsLetterId, String strBaseUrl, AdminUser user, Locale locale )
302     {
303         String strTemplatePath = NewsletterUtils.getHtmlTemplatePath( nTemplateNewsLetterId, getPlugin( ) );
304         // String strDocumentPath = generateDocumentsList( nNewsLetterId, nTemplateDocumentId, strBaseUrl );
305 
306         if ( strTemplatePath == null )
307         {
308             return null;
309         }
310 
311         Map<String, Object> model = new HashMap<String, Object>( );
312         List<NewsletterTopic> listTopics = NewsletterTopicHome.findAllByIdNewsletter( newsletter.getId( ), getPlugin( ) );
313 
314         // We sort the elements so that they are ordered by section and order.
315         Collections.sort( listTopics );
316 
317         int nCurrentSection = 0;
318         String [ ] strContentBySection = new String [ newsletter.getNbSections( )];
319         List<NewsletterTopic> listSelectedTopics = new ArrayList<NewsletterTopic>( );
320         for ( int i = 0; i < listTopics.size( ) + 1; i++ )
321         {
322             NewsletterTopic newsletterTopic = null;
323             if ( i < listTopics.size( ) )
324             {
325                 newsletterTopic = listTopics.get( i );
326             }
327             if ( newsletterTopic != null && newsletterTopic.getSection( ) == nCurrentSection )
328             {
329                 listSelectedTopics.add( newsletterTopic );
330             }
331             else
332             {
333                 if ( nCurrentSection != 0 )
334                 {
335                     StringBuilder sbSectionContent = new StringBuilder( );
336                     for ( NewsletterTopic topic : listSelectedTopics )
337                     {
338                         sbSectionContent.append( getNewsletterTopicService( ).getTopicContent( topic, user, locale ) );
339                     }
340                     if ( nCurrentSection - 1 < strContentBySection.length )
341                     {
342                         strContentBySection [nCurrentSection - 1] = sbSectionContent.toString( );
343                     }
344                 }
345                 if ( newsletterTopic != null )
346                 {
347                     nCurrentSection = newsletterTopic.getSection( );
348                     listSelectedTopics = new ArrayList<NewsletterTopic>( );
349                     listSelectedTopics.add( newsletterTopic );
350                 }
351             }
352         }
353 
354         model.put( NewsLetterConstants.MARK_CONTENT, strContentBySection [0] );
355         for ( int i = 0; i < strContentBySection.length; i++ )
356         {
357             model.put( NewsLetterConstants.MARK_CONTENT_SECTION + Integer.toString( i + 1 ), strContentBySection [i] );
358         }
359         model.put( NewsLetterConstants.MARK_BASE_URL, strBaseUrl );
360 
361         HtmlTemplate templateNewsLetter = new HtmlTemplate( );
362         String templateFileKey = NewsLetterTemplateHome.findByPrimaryKey( nTemplateNewsLetterId, getPlugin( ) ).getFileKey();
363            // if template file key can be parsed as an int, it means it is not the name of the file
364             if ( templateFileKey != null && StringUtils.isNumeric( templateFileKey ) )
365             {
366                 fr.paris.lutece.portal.business.file.File file = NewsletterFileService.getFileByKey( templateFileKey );
367                 byte[] _bTemplate = file.getPhysicalFile().getValue( );
368 
369                 // convert byte array to html
370                 String strTemplate = new String( _bTemplate );
371                 templateNewsLetter = AppTemplateService.getTemplateFromStringFtl( strTemplate, locale, model );
372 
373             }
374             else
375             {
376                  templateNewsLetter = AppTemplateService.getTemplate( strTemplatePath, locale, model );
377 
378             }
379 
380         return templateNewsLetter.getHtml( );
381     }
382     /**
383      * Get the url of the image folder used by templates
384      * 
385      * @param strBaseUrl
386      *            The base url
387      * @return The absolute url of the folder containing images of templates.
388      */
389     public String getImageFolderPath( String strBaseUrl )
390     {
391         return strBaseUrl + AppPropertiesService.getProperty( PROPERTY_PATH_IMAGE_NEWSLETTER_TEMPLATE );
392     }
393 
394     /**
395      * Check if images of the newsletter should be transfered on an unsecured webapp or not
396      * 
397      * @return True if images of the newsletter should be transfered on an unsecured webapp, false otherwise
398      */
399     public boolean useUnsecuredImages( )
400     {
401         return Boolean.parseBoolean( AppPropertiesService.getProperty( PROPERTY_NO_SECURED_IMG_OPTION ) );
402     }
403 
404     /**
405      * Get the unsecured image folder inside the unsecured folder
406      * 
407      * @return The unsecured image folder inside the unsecured folder
408      */
409     public String getUnsecuredImagefolder( )
410     {
411         return AppPropertiesService.getProperty( PROPERTY_NO_SECURED_IMG_FOLDER ) + NewsLetterConstants.CONSTANT_SLASH;
412     }
413 
414     /**
415      * Get the absolute path to the unsecured folder where files should be saved
416      * 
417      * @return The absolute path to the unsecured folder where files should be saved, or the webapp path if none is defined
418      */
419     public String getUnsecuredFolderPath( )
420     {
421         return AppPropertiesService.getProperty( PROPERTY_WEBAPP_PATH, AppPathService.getWebAppPath( ) + NewsLetterConstants.CONSTANT_SLASH );
422     }
423 
424     /**
425      * Get the absolute url to the unsecured webapp.
426      * 
427      * @return The absolute url to the unsecured webapp, or the base url of this webapp if none is defined
428      */
429     public String getUnsecuredWebappUrl( )
430     {
431         return AppPropertiesService.getProperty( PROPERTY_WEBAPP_URL, AppPathService.getBaseUrl( ) );
432     }
433 
434     /**
435      * Get the unsubscription key associated with the given email address.
436      * 
437      * @param strEmail
438      *            The email to get the unsubscription key of.
439      * @return The unsubscription key of the email
440      */
441     public String getUnsubscriptionKey( String strEmail )
442     {
443         return CryptoService.encrypt( CryptoService.getCryptoKey( ) + strEmail,
444                 AppPropertiesService.getProperty( PROPERTY_UNSUBSCRIBE_KEY_ENCRYPTION_ALGORITHM ) );
445     }
446 
447     /**
448      * Modify the number of sections of a newsletter template
449      * 
450      * @param nOldSectionNumber
451      *            The old number of sections
452      * @param nNewSectionNumber
453      *            The new number of sections
454      * @param nTemplateId
455      *            The id of the template
456      */
457     public void modifySectionNumber( int nOldSectionNumber, int nNewSectionNumber, int nTemplateId )
458     {
459         // If the number of sections changed and is valid
460         if ( nOldSectionNumber != nNewSectionNumber && nNewSectionNumber > 0 )
461         {
462             Collection<NewsLetter> listNewsletters = NewsLetterHome.findAllByTemplateId( nTemplateId, getPlugin( ) );
463             for ( NewsLetter newsletter : listNewsletters )
464             {
465                 // If we removed sections we reorganize newsletter's topics
466                 if ( nOldSectionNumber > nNewSectionNumber )
467                 {
468                     List<NewsletterTopic> listTopics = NewsletterTopicHome.findAllByIdNewsletter( newsletter.getId( ), getPlugin( ) );
469                     int nNewOrder = NewsletterTopicHome.getNewOrder( newsletter.getId( ), nNewSectionNumber, getPlugin( ) );
470                     for ( NewsletterTopic topic : listTopics )
471                     {
472                         if ( topic.getSection( ) > nNewSectionNumber )
473                         {
474                             topic.setSection( nNewSectionNumber );
475                             topic.setOrder( nNewOrder );
476                             nNewOrder++;
477                             NewsletterTopicHome.updateNewsletterTopic( topic, getPlugin( ) );
478                         }
479                     }
480                 }
481                 newsletter.setNbSections( nNewSectionNumber );
482                 NewsLetterHome.update( newsletter, getPlugin( ) );
483             }
484         }
485     }
486 
487     /**
488      * Get the instance of the newsletter plugin
489      * 
490      * @return the instance of the newsletter plugin
491      */
492     private Plugin getPlugin( )
493     {
494         return PluginService.getPlugin( NewsletterPlugin.PLUGIN_NAME );
495     }
496 
497     /**
498      * Get the NewsletterTopicService instance of this service
499      * 
500      * @return The NewsletterTopicService instance of this service
501      */
502     private NewsletterTopicService getNewsletterTopicService( )
503     {
504         if ( _newsletterTopicService == null )
505         {
506             _newsletterTopicService = NewsletterTopicService.getService( );
507         }
508         return _newsletterTopicService;
509     }
510 
511     /**
512      * Copy existing newsletter without its subscribers.
513      * 
514      * @param newsletter
515      *            newsletter to copy
516      * @param user
517      *            the current user
518      * @param locale
519      *            The locale
520      */
521     public void copyExistingNewsletter( NewsLetter newsletter, AdminUser user, Locale locale )
522     {
523         int oldNewsLetterId = newsletter.getId( );
524         NewsLetterHome.create( newsletter, getPlugin( ) );
525 
526         // Copy of topics
527         List<NewsletterTopic> topicList = NewsletterTopicHome.findAllByIdNewsletter( oldNewsLetterId, getPlugin( ) );
528         topicList.stream( ).forEach( ( NewsletterTopic nt ) -> {
529             int oldTopicId = nt.getId( );
530             nt.setIdNewsletter( newsletter.getId( ) );
531             for ( INewsletterTopicService service : SpringContextService.getBeansOfType( INewsletterTopicService.class ) )
532             {
533                 if ( StringUtils.equals( service.getNewsletterTopicTypeCode( ), nt.getTopicTypeCode( ) ) )
534                 {
535                     NewsletterTopicHome.insertNewsletterTopic( nt, getPlugin( ) );
536                     service.copyNewsletterTopic( oldTopicId, nt, user, locale );
537                 }
538             }
539         } );
540     }
541 }