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