├── .gitattributes ├── .gitignore ├── .jscs.json ├── .jshintrc ├── .php_cs ├── .scrutinizer.yml ├── .travis.yml ├── 3dparty └── Html2Text.php ├── Classes ├── BounceHandler.php ├── Controller │ ├── BounceAccountController.php │ ├── EmailController.php │ ├── LinkController.php │ ├── ModuleController.php │ ├── NewsletterController.php │ └── RecipientListController.php ├── Domain │ ├── Model │ │ ├── BounceAccount.php │ │ ├── Email.php │ │ ├── EmailInterface.php │ │ ├── IPlainConverter.php │ │ ├── Link.php │ │ ├── Newsletter.php │ │ ├── PlainConverter │ │ │ ├── Builtin.php │ │ │ └── Lynx.php │ │ ├── RecipientList.php │ │ └── RecipientList │ │ │ ├── AbstractArray.php │ │ │ ├── BeUsers.php │ │ │ ├── CsvFile.php │ │ │ ├── CsvList.php │ │ │ ├── CsvUrl.php │ │ │ ├── FeGroups.php │ │ │ ├── FePages.php │ │ │ ├── GentleSql.php │ │ │ ├── Html.php │ │ │ └── Sql.php │ └── Repository │ │ ├── AbstractRepository.php │ │ ├── BounceAccountRepository.php │ │ ├── EmailRepository.php │ │ ├── LinkRepository.php │ │ ├── NewsletterRepository.php │ │ └── RecipientListRepository.php ├── Exception.php ├── MVC │ ├── Controller │ │ └── ExtDirectActionController.php │ ├── ExtDirect │ │ ├── Api.php │ │ ├── Exception │ │ │ └── InvalidExtDirectRequestException.php │ │ ├── Request.php │ │ ├── RequestBuilder.php │ │ ├── RequestHandler.php │ │ ├── Transaction.php │ │ └── TransactionResponse.php │ └── View │ │ ├── ExtDirectView.php │ │ └── JsonView.php ├── Mailer.php ├── Task │ ├── FetchBounces.php │ └── SendEmails.php ├── Tca │ ├── BounceAccountDataProvider.php │ ├── BounceAccountTca.php │ ├── EmailTca.php │ └── RecipientListTca.php ├── Tools.php ├── Update │ ├── Transaction.php │ ├── TransactionResult.php │ └── Update.php ├── Utility │ ├── EmailParser.php │ ├── MarkerSubstitutor.php │ ├── Uri.php │ ├── UriBuilder.php │ └── Validator.php └── ViewHelpers │ ├── AbstractViewHelper.php │ ├── Be │ └── ModuleContainerViewHelper.php │ ├── ConfigurationViewHelper.php │ ├── CsvValuesViewHelper.php │ ├── ExtDirectProviderViewHelper.php │ ├── IncludeCssFileViewHelper.php │ ├── IncludeExtOnReadyFromFileViewHelper.php │ ├── IncludeJsFileViewHelper.php │ ├── IncludeJsFolderViewHelper.php │ ├── IncludeModuleBodyViewHelper.php │ └── LocalizationViewHelper.php ├── Configuration ├── TCA │ ├── tx_newsletter_domain_model_bounceaccount.php │ ├── tx_newsletter_domain_model_email.php │ ├── tx_newsletter_domain_model_link.php │ ├── tx_newsletter_domain_model_newsletter.php │ └── tx_newsletter_domain_model_recipientlist.php └── TypoScript │ ├── constants.txt │ └── setup.txt ├── Documentation ├── ChangeLog │ └── Index.rst ├── Configuration │ ├── Index.rst │ └── Recipient_List_SQL_Examples.rst ├── Developer │ └── Index.rst ├── Doxyfile ├── Images │ ├── Newsletter_-_Sending.png │ ├── Newsletter_-_Settings.png │ ├── Newsletter_-_Status.png │ ├── Overview_-_small.png │ ├── Statistics_-_Emails.png │ ├── Statistics_-_Links.png │ ├── Statistics_-_Overview.png │ └── model.png ├── Includes.txt ├── Index.rst ├── Introduction │ └── Index.rst ├── Links.rst ├── Settings.yml ├── User │ └── Index.rst └── gui mockup │ ├── background.png │ ├── background_usertools.png │ ├── bullet_toggle_minus.png │ ├── bullet_toggle_plus.png │ ├── calendar.png │ ├── chart_bar.png │ ├── chart_curve.png │ ├── chart_pie.png │ ├── diagnostic-linkcheck.png │ ├── diagnostic.png │ ├── eye.png │ ├── general-stats.png │ ├── newsletter.svg │ ├── piechart.png │ ├── stats-click-graph.png │ ├── stats-general-graph-header.png │ ├── stats-general-graph.png │ └── stats-general.png ├── README.rst ├── Resources ├── Private │ ├── .htaccess │ ├── Language │ │ ├── de.locallang.xlf │ │ ├── de.locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf │ │ ├── de.locallang_csh_tx_newsletter_domain_model_email.xlf │ │ ├── de.locallang_csh_tx_newsletter_domain_model_link.xlf │ │ ├── de.locallang_csh_tx_newsletter_domain_model_newsletter.xlf │ │ ├── de.locallang_db.xlf │ │ ├── de.locallang_module.xlf │ │ ├── fr.locallang.xlf │ │ ├── fr.locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf │ │ ├── fr.locallang_csh_tx_newsletter_domain_model_email.xlf │ │ ├── fr.locallang_csh_tx_newsletter_domain_model_link.xlf │ │ ├── fr.locallang_csh_tx_newsletter_domain_model_newsletter.xlf │ │ ├── fr.locallang_csh_tx_newsletter_domain_model_recipientlist.xlf │ │ ├── fr.locallang_db.xlf │ │ ├── fr.locallang_module.xlf │ │ ├── locallang.xlf │ │ ├── locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf │ │ ├── locallang_csh_tx_newsletter_domain_model_email.xlf │ │ ├── locallang_csh_tx_newsletter_domain_model_link.xlf │ │ ├── locallang_csh_tx_newsletter_domain_model_newsletter.xlf │ │ ├── locallang_csh_tx_newsletter_domain_model_recipientlist.xlf │ │ ├── locallang_db.xlf │ │ └── locallang_module.xlf │ ├── Templates │ │ ├── Email │ │ │ ├── Show.html │ │ │ └── Unsubscribe.html │ │ ├── Module │ │ │ └── Index.html │ │ └── RecipientList │ │ │ ├── Export.csv │ │ │ ├── Export.html │ │ │ └── Export.xml │ └── clear.gif └── Public │ ├── Icons │ ├── error.svg │ ├── information.svg │ ├── ok.svg │ ├── pie_chart.svg │ ├── test-können 한국어 ✓ € ☺ ☹.svg │ ├── tx_newsletter.svg │ ├── tx_newsletter_domain_model_bounceaccount.svg │ ├── tx_newsletter_domain_model_email.svg │ ├── tx_newsletter_domain_model_link.svg │ ├── tx_newsletter_domain_model_newsletter.svg │ ├── tx_newsletter_domain_model_recipientlist.svg │ └── warning.svg │ ├── JavaScript │ ├── DirectFlashMessageDispatcher.js │ ├── ExtOnReady.js │ ├── Module │ │ ├── Application.js │ │ └── FlashMessageOverlayContainer.js │ ├── NVD3 │ │ ├── d3.v3.js │ │ └── nv.d3.js │ ├── Override │ │ ├── DirectProxyPatch.js │ │ └── Ext.ux.form.DateTime.js │ ├── Planner │ │ └── Planner.js │ ├── Statistics │ │ ├── NewsletterListMenu.js │ │ ├── Statistics.js │ │ ├── StatisticsPanel.js │ │ └── StatisticsPanel │ │ │ ├── EmailTab.js │ │ │ ├── LinkTab.js │ │ │ └── OverviewTab.js │ └── Store │ │ ├── BounceAccount.js │ │ ├── Email.js │ │ ├── Link.js │ │ ├── Newsletter.js │ │ ├── PlannedNewsletter.js │ │ ├── Recipient.js │ │ ├── RecipientList.js │ │ └── SelectedNewsletter.js │ └── Styles │ ├── flashmessages.css │ ├── module.css │ └── nv.d3.css ├── Tests ├── Build │ ├── FunctionalTests.xml │ ├── FunctionalTestsBootstrap.php │ ├── UnitTests.xml │ └── UnitTestsBootstrap.php ├── Functional │ ├── AbstractFunctionalTestCase.php │ ├── BounceHandlerTest.php │ ├── Fixtures │ │ ├── fixtures.xml │ │ └── mailer │ │ │ ├── input.html │ │ │ ├── output-2-false-false.eml │ │ │ ├── output-2-false-true.eml │ │ │ ├── output-2-true-false.eml │ │ │ ├── output-2-true-true.eml │ │ │ └── output-6-true-true.eml │ ├── MailerTest.php │ ├── Repository │ │ ├── BounceAccountRepositoryTest.php │ │ ├── EmailRepositoryTest.php │ │ ├── LinkRepositoryTest.php │ │ ├── NewsletterRepositoryTest.php │ │ └── RecipientListRepositoryTest.php │ ├── ToolsTest.php │ └── Utility │ │ └── UriBuilderTest.php └── Unit │ ├── AbstractUnitTestCase.php │ ├── Controller │ ├── BounceAccountControllerTest.php │ └── NewsletterControllerTest.php │ ├── Domain │ └── Model │ │ ├── BounceAccountTest.php │ │ ├── EmailTest.php │ │ ├── LinkTest.php │ │ ├── NewsletterTest.php │ │ ├── PlainConverter │ │ ├── BuiltinTest.php │ │ ├── LynxTest.php │ │ ├── builtin.txt │ │ ├── input.html │ │ └── lynx.txt │ │ └── RecipientList │ │ ├── AbstractRecipientList.php │ │ ├── CsvFileTest.php │ │ ├── CsvListTest.php │ │ ├── CsvUrlTest.php │ │ ├── SqlTest.php │ │ └── data.csv │ ├── Fixtures │ └── bounce │ │ ├── 2-87c4e9b09085befbb7f20faa7482213a-Undelivered Mail Returned to Sender.eml │ │ └── 4-12548ebea88c16a5aa62096db4d0b258-Automatic unsubscribe via mail.live.com.eml │ ├── ToolsTest.php │ └── Utility │ ├── EmailParserTest.php │ ├── UriTest.php │ └── ValidatorTest.php ├── class.ext_update.php ├── cli ├── .htaccess └── bounce.php ├── composer.json ├── composer.lock ├── ext_conf_template.txt ├── ext_emconf.php ├── ext_icon.svg ├── ext_localconf.php ├── ext_tables.php ├── ext_tables.sql ├── gulpfile.js ├── package.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.eml eol=crlf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Documentation/html 2 | /Documentation/gui mockup/newsletter_variante2_diagnostic.svg.png 3 | /Documentation/gui mockup/newsletter_variante2_general_01.svg.png 4 | /Documentation/gui mockup/newsletter_variante2_general_02.svg.png 5 | /Documentation/gui mockup/newsletter_variante2_stats_0.svg.png 6 | /Documentation/gui mockup/newsletter_variante2_stats_1.svg.png 7 | /Documentation/gui mockup/newsletter_variante2_stats_2.svg.png 8 | /Documentation/gui mockup/newsletter_variante2_stats.svg 9 | /Documentation/gui mockup/newslettter.png 10 | /node_modules/ 11 | /vendor/ 12 | /web/ 13 | /.idea/ 14 | /.php_cs.cache 15 | -------------------------------------------------------------------------------- /.jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch" 10 | ], 11 | "requireSpaceAfterKeywords": [ 12 | "if", 13 | "else", 14 | "for", 15 | "while", 16 | "do", 17 | "switch", 18 | "return", 19 | "try", 20 | "catch" 21 | ], 22 | "disallowSpaceBeforeBinaryOperators": [ 23 | "," 24 | ], 25 | "disallowImplicitTypeConversion": [ 26 | "string" 27 | ], 28 | "disallowKeywords": [ 29 | "with" 30 | ], 31 | "disallowMultipleLineBreaks": true, 32 | "disallowKeywordsOnNewLine": [ 33 | "else" 34 | ], 35 | "disallowMixedSpacesAndTabs": true, 36 | "disallowTrailingWhitespace": true, 37 | "requireLineFeedAtFileEnd": true, 38 | "requireSpacesInFunctionExpression": { 39 | "beforeOpeningCurlyBrace": true 40 | }, 41 | "disallowSpacesInFunctionExpression": { 42 | "beforeOpeningRoundBrace": true 43 | }, 44 | "disallowSpacesInsideArrayBrackets": true, 45 | "disallowSpacesInsideParentheses": true, 46 | "validateJSDoc": { 47 | "checkParamNames": true 48 | }, 49 | "validateIndentation": 4, 50 | "requireSpaceBeforeBinaryOperators": [ 51 | "?", 52 | "+", 53 | "-", 54 | "/", 55 | "*", 56 | "=", 57 | "==", 58 | "===", 59 | "!=", 60 | "!==", 61 | ">", 62 | ">=", 63 | "<", 64 | "<=" 65 | ], 66 | "requireSpaceAfterBinaryOperators": [ 67 | "?", 68 | "+", 69 | "-", 70 | "/", 71 | "*", 72 | "=", 73 | "==", 74 | "===", 75 | "!=", 76 | "!==", 77 | ">", 78 | ">=", 79 | "<", 80 | "<=" 81 | ], 82 | "disallowSpaceAfterPrefixUnaryOperators": [ 83 | "++", 84 | "--", 85 | "+", 86 | "-", 87 | "~", 88 | "!" 89 | ], 90 | "disallowSpaceBeforePostfixUnaryOperators": [ 91 | "++", 92 | "--" 93 | ], 94 | "disallowSpaceAfterObjectKeys": true, 95 | "validateLineBreaks": "LF", 96 | "requireDotNotation": true, 97 | "requireSpacesInConditionalExpression": true, 98 | "disallowEmptyBlocks": true, 99 | "requireBlocksOnNewline": true, 100 | "requireSpaceBeforeBlockStatements": true 101 | } 102 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | // Custom globals. 4 | "Ext", 5 | "nv", 6 | "d3" 7 | ], 8 | // On-line example https://gist.github.com/connor/1597131 9 | "bitwise": true, 10 | // Prohibit bitwise operators (&, |, ^, etc.). 11 | "browser": true, 12 | // Standard browser globals e.g. `window`, `document`. 13 | "camelcase": false, 14 | "curly": true, 15 | // Require {} for every new block or scope. 16 | "evil": true, 17 | // Allow eval() for custom template of 18 | "forin": false, 19 | // Tolerate `for in` loops without `hasOwnPrototype`. 20 | "indent": 4, 21 | // Specify indentation spacing 22 | "immed": true, 23 | "jquery": true, 24 | "latedef": true, 25 | "newcap": true, 26 | "noarg": true, 27 | // Prohibit use of `arguments.caller` and `arguments.callee` 28 | "noempty": true, 29 | // Prohibit use of empty blocks. 30 | "nonew": true, 31 | // Prohibit use of constructors for side-effects. 32 | "strict": true, 33 | // Require `use strict` pragma in every file. 34 | "undef": true, 35 | // Require all non-global variables be declared before they are used. 36 | "unused": true 37 | } 38 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | php: true 3 | 4 | coding_style: 5 | php: 6 | spaces: 7 | before_parentheses: 8 | closure_definition: true 9 | around_operators: 10 | concatenation: true 11 | 12 | build: 13 | nodes: 14 | analysis: 15 | tests: 16 | override: 17 | - php-scrutinizer-run 18 | 19 | tools: 20 | external_code_coverage: 21 | timeout: 900 # Timeout in seconds 22 | runs: 2 # Scrutinizer will wait for code coverage of unit and functional tests 23 | 24 | build_failure_conditions: 25 | - 'elements.rating(<= C).new.exists' # No new classes/methods with a rating of C or worse allowed 26 | - 'issues.severity(>= MAJOR).new.exists' # New issues of major or higher severity 27 | - 'project.metric_change("scrutinizer.test_coverage", < 0)' # Code Coverage decreased from previous inspection 28 | - 'patches.label("Unused Use Statements").new.exists' # No new unused imports patches allowed 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | dist: trusty 3 | 4 | addons: 5 | apt: 6 | packages: 7 | - mysql-server-5.6 8 | - mysql-client-core-5.6 9 | - mysql-client-5.6 10 | 11 | php: 12 | - 5.6 13 | - 7.0 14 | - 7.1 15 | - 7.2 16 | 17 | env: 18 | - TYPO3_VERSION=7.6.25 19 | - TYPO3_VERSION=master 20 | 21 | matrix: 22 | fast_finish: true 23 | allow_failures: 24 | - env: TYPO3_VERSION=master 25 | 26 | cache: 27 | directories: 28 | - vendor 29 | - $HOME/.composer/cache 30 | 31 | before_install: 32 | - nvm install 8 33 | - nvm use 8 34 | - curl -o- -L https://yarnpkg.com/install.sh | bash 35 | - export PATH=$HOME/.yarn/bin:$PATH 36 | 37 | before_script: 38 | - if [[ $TRAVIS_PHP_VERSION = '7.1' && $TYPO3_VERSION = '7.6.25' ]]; then PHPUNIT_FLAGS_UNIT="--coverage-clover=unit-tests-coverage.clover"; else PHPUNIT_FLAGS_UNIT=""; fi 39 | - if [[ $TRAVIS_PHP_VERSION = '7.1' && $TYPO3_VERSION = '7.6.25' ]]; then PHPUNIT_FLAGS_FUNCTIONAL="--coverage-clover=functional-tests-coverage.clover"; else PHPUNIT_FLAGS_FUNCTIONAL=""; fi 40 | - yarn install 41 | - composer install 42 | - cd .. 43 | - git clone --branch $TYPO3_VERSION --depth 1 https://github.com/TYPO3/TYPO3.CMS.git typo3_core 44 | - mv typo3_core/* . 45 | - composer install 46 | - mkdir -p uploads typo3temp typo3conf/ext 47 | - mv newsletter typo3conf/ext/ 48 | - export typo3DatabaseName="typo3"; 49 | - export typo3DatabaseHost="localhost"; 50 | - export typo3DatabaseUsername="root"; 51 | - export typo3DatabasePassword=""; 52 | 53 | script: 54 | - cd typo3conf/ext/newsletter && ./node_modules/.bin/gulp && cd ../../../ 55 | - ./bin/phpunit $PHPUNIT_FLAGS_UNIT --colors -c typo3conf/ext/newsletter/Tests/Build/UnitTests.xml 56 | - ./bin/phpunit $PHPUNIT_FLAGS_FUNCTIONAL --colors -c typo3conf/ext/newsletter/Tests/Build/FunctionalTests.xml 57 | 58 | after_script: 59 | - if [[ ! -z $PHPUNIT_FLAGS_UNIT ]]; then echo "Uploading code coverage results" && cp -R typo3conf/ext/newsletter/.git . && wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover unit-tests-coverage.clover && php ocular.phar code-coverage:upload --format=php-clover functional-tests-coverage.clover ; fi 60 | -------------------------------------------------------------------------------- /Classes/Controller/BounceAccountController.php: -------------------------------------------------------------------------------- 1 | bounceAccountRepository = $bounceAccountRepository; 29 | } 30 | 31 | /** 32 | * Displays all BounceAccounts 33 | * 34 | * @return string The rendered list view 35 | */ 36 | public function listAction() 37 | { 38 | $bounceAccounts = $this->bounceAccountRepository->findAll(); 39 | 40 | $this->view->setVariablesToRender(['total', 'data', 'success', 'flashMessages']); 41 | $this->view->setConfiguration([ 42 | 'data' => [ 43 | '_descendAll' => self::resolveJsonViewConfiguration(), 44 | ], 45 | ]); 46 | 47 | $this->addFlashMessage('Loaded BounceAccounts from Server side.', 'BounceAccounts loaded successfully', FlashMessage::NOTICE); 48 | 49 | $this->view->assign('total', $bounceAccounts->count()); 50 | $this->view->assign('data', $bounceAccounts); 51 | $this->view->assign('success', true); 52 | $this->flushFlashMessages(); 53 | } 54 | 55 | /** 56 | * Returns a configuration for the JsonView, that describes which fields should be rendered for 57 | * a BounceAccount record. 58 | * 59 | * @return array 60 | */ 61 | public static function resolveJsonViewConfiguration() 62 | { 63 | return [ 64 | '_exposeObjectIdentifier' => true, 65 | '_only' => [ 66 | 'email', 67 | 'server', 68 | 'protocol', 69 | 'username', 70 | ], 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Classes/Controller/ModuleController.php: -------------------------------------------------------------------------------- 1 | pageId = (int) GeneralUtility::_GP('id'); 26 | } 27 | 28 | /** 29 | * index action for the module controller 30 | * This will render the HTML needed for ExtJS application 31 | */ 32 | public function indexAction() 33 | { 34 | $pageType = ''; 35 | $record = Tools::getDatabaseConnection()->exec_SELECTgetSingleRow('doktype', 'pages', 'uid =' . $this->pageId); 36 | if (!empty($record['doktype']) && $record['doktype'] == 254) { 37 | $pageType = 'folder'; 38 | } elseif (!empty($record['doktype'])) { 39 | $pageType = 'page'; 40 | } 41 | 42 | $configuration = [ 43 | 'pageId' => $this->pageId, 44 | 'pageType' => $pageType, 45 | 'emailShowUrl' => UriBuilder::buildFrontendUri($this->pageId, 'Email', 'show'), 46 | ]; 47 | 48 | $this->view->assign('configuration', $configuration); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Classes/Domain/Model/EmailInterface.php: -------------------------------------------------------------------------------- 1 | objectManager = GeneralUtility::makeInstance(ObjectManager::class); 51 | } 52 | 53 | /** 54 | * Setter for url 55 | * 56 | * @param string $url url 57 | */ 58 | public function setUrl($url) 59 | { 60 | $this->url = $url; 61 | } 62 | 63 | /** 64 | * Getter for url 65 | * 66 | * @return string url 67 | */ 68 | public function getUrl() 69 | { 70 | return $this->url; 71 | } 72 | 73 | /** 74 | * Setter for newsletter 75 | * 76 | * @param Newsletter $newsletter newsletter 77 | */ 78 | public function setNewsletter(Newsletter $newsletter) 79 | { 80 | $this->newsletter = $newsletter; 81 | } 82 | 83 | /** 84 | * Getter for newsletter 85 | * 86 | * @return Newsletter newsletter 87 | */ 88 | public function getNewsletter() 89 | { 90 | $newsletterRepository = $this->objectManager->get(NewsletterRepository::class); 91 | 92 | return $newsletterRepository->findByUid($this->newsletter); 93 | } 94 | 95 | /** 96 | * Setter for openedCount 97 | * 98 | * @param int $openedCount openedCount 99 | */ 100 | public function setOpenedCount($openedCount) 101 | { 102 | $this->openedCount = $openedCount; 103 | } 104 | 105 | /** 106 | * Getter for openedCount 107 | * 108 | * @return int openedCount 109 | */ 110 | public function getOpenedCount() 111 | { 112 | return $this->openedCount; 113 | } 114 | 115 | public function getOpenedPercentage() 116 | { 117 | $emailRepository = $this->objectManager->get(EmailRepository::class); 118 | $emailCount = $emailRepository->getCount($this->newsletter); 119 | 120 | if ($emailCount == 0) { 121 | return 0; 122 | } 123 | 124 | return round($this->getOpenedCount() * 100 / $emailCount, 2); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Classes/Domain/Model/PlainConverter/Builtin.php: -------------------------------------------------------------------------------- 1 | 'table', 20 | ]); 21 | $converter->setBaseUrl($baseUrl); 22 | 23 | return $converter->getText(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Classes/Domain/Model/PlainConverter/Lynx.php: -------------------------------------------------------------------------------- 1 | injectBaseUrl($content, $baseUrl); 26 | 27 | file_put_contents($tmpFile, $contentWithBase); 28 | 29 | $cmd = escapeshellcmd(Tools::confParam('path_to_lynx')) . ' -force_html -dump ' . escapeshellarg($tmpFile); 30 | exec($cmd, $output); 31 | unlink($tmpFile); 32 | $plainText = implode("\n", $output); 33 | 34 | return $plainText; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RecipientList/AbstractArray.php: -------------------------------------------------------------------------------- 1 | data); 17 | next($this->data); 18 | 19 | if (is_array($r)) { 20 | if (!isset($r['plain_only'])) { 21 | $r['plain_only'] = $this->getPlainOnly(); 22 | } 23 | 24 | return $r; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | public function getCount() 31 | { 32 | return count($this->data); 33 | } 34 | 35 | public function getError() 36 | { 37 | if (!is_array($this->data)) { 38 | return 'Not an array'; 39 | } 40 | 41 | if (count($this->data) == 0) { 42 | return 'No data fetched'; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RecipientList/BeUsers.php: -------------------------------------------------------------------------------- 1 | beUsers = $beUsers; 27 | } 28 | 29 | /** 30 | * Getter for beUsers 31 | * 32 | * @return string beUsers 33 | */ 34 | public function getBeUsers() 35 | { 36 | return $this->beUsers; 37 | } 38 | 39 | /** 40 | * Returns the tablename to work with 41 | * 42 | * @return string 43 | */ 44 | protected function getTableName() 45 | { 46 | return 'be_users'; 47 | } 48 | 49 | public function init() 50 | { 51 | $config = explode(',', $this->getBeUsers()); 52 | $config[] = -1; 53 | $config = array_filter($config); 54 | 55 | $this->data = Tools::getDatabaseConnection()->sql_query( 56 | 'SELECT email, realName, username, lang, admin FROM be_users 57 | WHERE uid IN (' . implode(',', $config) . ") 58 | AND email <> '' 59 | AND disable = 0 60 | AND tx_newsletter_bounce < 10"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RecipientList/CsvList.php: -------------------------------------------------------------------------------- 1 | csvValues = $csvValues; 25 | } 26 | 27 | /** 28 | * Getter for csvValues 29 | * 30 | * @return string csvValues 31 | */ 32 | public function getCsvValues() 33 | { 34 | return $this->csvValues; 35 | } 36 | 37 | public function init() 38 | { 39 | $this->loadCsvFromData($this->getCsvValues()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RecipientList/CsvUrl.php: -------------------------------------------------------------------------------- 1 | csvUrl = $csvUrl; 25 | } 26 | 27 | /** 28 | * Getter for csvUrl 29 | * 30 | * @return string csvUrl 31 | */ 32 | public function getCsvUrl() 33 | { 34 | return $this->csvUrl; 35 | } 36 | 37 | public function init() 38 | { 39 | $this->loadCsvFromFile($this->getCsvUrl()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RecipientList/FeGroups.php: -------------------------------------------------------------------------------- 1 | feGroups = $feGroups; 27 | } 28 | 29 | /** 30 | * Getter for feGroups 31 | * 32 | * @return string feGroups 33 | */ 34 | public function getFeGroups() 35 | { 36 | return $this->feGroups; 37 | } 38 | 39 | /** 40 | * Returns the tablename to work with 41 | * 42 | * @return string 43 | */ 44 | protected function getTableName() 45 | { 46 | return 'fe_users'; 47 | } 48 | 49 | public function init() 50 | { 51 | $groups = explode(',', $this->getFeGroups()); 52 | $groups[] = -1; 53 | $groups = array_filter($groups); 54 | 55 | $this->data = Tools::getDatabaseConnection()->sql_query( 56 | 'SELECT DISTINCT email,name,address,telephone,fax,username,fe_users.title,zip,city,country,www,company,fe_groups.title AS group_title 57 | FROM fe_groups, fe_users 58 | WHERE fe_groups.uid IN (' . implode(',', $groups) . ") 59 | AND FIND_IN_SET(fe_groups.uid, fe_users.usergroup) 60 | AND email != '' 61 | AND fe_groups.deleted = 0 62 | AND fe_groups.hidden = 0 63 | AND fe_users.disable = 0 64 | AND fe_users.deleted = 0 65 | AND tx_newsletter_bounce < 10"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RecipientList/FePages.php: -------------------------------------------------------------------------------- 1 | fePages = $fePages; 27 | } 28 | 29 | /** 30 | * Getter for fePages 31 | * 32 | * @return string fePages 33 | */ 34 | public function getFePages() 35 | { 36 | return $this->fePages; 37 | } 38 | 39 | /** 40 | * Returns the tablename to work with 41 | * 42 | * @return string 43 | */ 44 | protected function getTableName() 45 | { 46 | return 'fe_users'; 47 | } 48 | 49 | public function init() 50 | { 51 | $config = explode(',', $this->getFePages()); 52 | $config[] = -1; 53 | $config = array_filter($config); 54 | 55 | $this->data = Tools::getDatabaseConnection()->sql_query( 56 | 'SELECT DISTINCT email,name,address,telephone,fax,username,fe_users.title,zip,city,country,www,company,pages.title AS pages_title 57 | FROM pages 58 | INNER JOIN fe_users ON pages.uid = fe_users.pid 59 | WHERE pages.uid IN (' . implode(',', $config) . ") 60 | AND email != '' 61 | AND pages.deleted = 0 62 | AND pages.hidden = 0 63 | AND fe_users.disable = 0 64 | AND fe_users.deleted = 0 65 | AND tx_newsletter_bounce < 10"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RecipientList/GentleSql.php: -------------------------------------------------------------------------------- 1 | getTableName() table. 11 | */ 12 | abstract class GentleSql extends Sql 13 | { 14 | /** 15 | * Returns the tablename to work with 16 | * 17 | * @return string 18 | */ 19 | abstract protected function getTableName(); 20 | 21 | /** 22 | * This increases the bounce-counter each time a mail has bounced. 23 | * Hard bounces count more that soft ones. After 2 hards or 10 softs the user will be disabled. 24 | * You should be able to reset then in the backend 25 | * 26 | * @param string $email the email address of the recipient 27 | * @param int $bounceLevel this is the level of the bounce 28 | * 29 | * @return bool success of the bounce-handling 30 | */ 31 | public function registerBounce($email, $bounceLevel) 32 | { 33 | $db = Tools::getDatabaseConnection(); 34 | 35 | $increment = 0; 36 | switch ($bounceLevel) { 37 | case EmailParser::NEWSLETTER_UNSUBSCRIBE: 38 | $increment = 10; 39 | break; 40 | case EmailParser::NEWSLETTER_HARDBOUNCE: 41 | $increment = 5; 42 | break; 43 | case EmailParser::NEWSLETTER_SOFTBOUNCE: 44 | $increment = 1; 45 | break; 46 | } 47 | 48 | if ($increment) { 49 | $db->sql_query('UPDATE ' . $this->getTableName() . " 50 | SET tx_newsletter_bounce = tx_newsletter_bounce + $increment 51 | WHERE email = '$email'"); 52 | 53 | return $db->sql_affected_rows(); 54 | } 55 | 56 | return false; 57 | } 58 | 59 | /** 60 | * This is a default action for registered clicks. 61 | * Here we just reset the bounce counter. If the user reads the mail, it must have succeded. 62 | * It can also be used for marketing or statistics purposes 63 | * 64 | * @param string $email the email address of the recipient 65 | */ 66 | public function registerClick($email) 67 | { 68 | Tools::getDatabaseConnection()->sql_query('UPDATE ' . $this->getTableName() . " 69 | SET tx_newsletter_bounce = 0 70 | WHERE email = '$email'"); 71 | } 72 | 73 | /** 74 | * Like the registerClick()-method, but just for embedded spy-image. 75 | * 76 | * @param string $email the email address of the recipient 77 | */ 78 | public function registerOpen($email) 79 | { 80 | Tools::getDatabaseConnection()->sql_query('UPDATE ' . $this->getTableName() . " 81 | SET tx_newsletter_bounce = 0 82 | WHERE email = '$email'"); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RecipientList/Html.php: -------------------------------------------------------------------------------- 1 | htmlUrl = $htmlUrl; 34 | } 35 | 36 | /** 37 | * Getter for htmlUrl 38 | * 39 | * @return string htmlUrl 40 | */ 41 | public function getHtmlUrl() 42 | { 43 | return $this->htmlUrl; 44 | } 45 | 46 | /** 47 | * Setter for htmlFetchType 48 | * 49 | * @param string $htmlFetchType htmlFetchType 50 | */ 51 | public function setHtmlFetchType($htmlFetchType) 52 | { 53 | $this->htmlFetchType = $htmlFetchType; 54 | } 55 | 56 | /** 57 | * Getter for htmlFetchType 58 | * 59 | * @return string htmlFetchType 60 | */ 61 | public function getHtmlFetchType() 62 | { 63 | return $this->htmlFetchType; 64 | } 65 | 66 | public function init() 67 | { 68 | $this->data = []; 69 | 70 | $content = Tools::getUrl($this->getHtmlUrl()); 71 | 72 | switch ($this->getHtmlFetchType()) { 73 | case 'mailto': 74 | preg_match_all('|]+href="mailto:([^"]+)"[^>]*>(.*)|Ui', $content, $fetched_data); 75 | 76 | foreach ($fetched_data[1] as $i => $email) { 77 | $this->data[] = ['email' => $email, 'name' => $fetched_data[2][$i]]; 78 | } 79 | break; 80 | case 'regex': 81 | default: 82 | preg_match_all("|[\.a-z0-9!#$%&'*+-/=?^_`{\|}]+@[a-z0-9_-][\.a-z0-9_-]*\.[a-z]{2,}|i", $content, $fetched_data); 83 | 84 | foreach ($fetched_data[0] as $address) { 85 | $this->data[]['email'] = $address; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/AbstractRepository.php: -------------------------------------------------------------------------------- 1 | getQuerySettings()->setRespectStoragePage(false); 22 | 23 | return $query; 24 | } 25 | 26 | /** 27 | * Override parent method to update the object and persist changes immediately. By commiting immediately 28 | * stay compatible with raw sql query via $TYPO3_DB. 29 | * TODO this method should be destroyed once "old code" is completely replaced with extbase concepts 30 | * 31 | * @param \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $modifiedObject 32 | */ 33 | public function update($modifiedObject) 34 | { 35 | parent::update($modifiedObject); 36 | $this->persistenceManager->persistAll(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/BounceAccountRepository.php: -------------------------------------------------------------------------------- 1 | get(ConfigurationManager::class); 22 | $storagePid = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'newsletter', 'storagePid'); 23 | 24 | if ($storagePid['storagePid']) { 25 | $query->getQuerySettings()->setRespectStoragePage(true); 26 | } 27 | 28 | return $query; 29 | } 30 | 31 | /** 32 | * Returns the first BounceAccount or null if none at all 33 | * 34 | * @return BounceAccount 35 | */ 36 | public function findFirst() 37 | { 38 | $query = $this->createQuery(); 39 | 40 | $bounceAccount = $query->setLimit(1) 41 | ->execute() 42 | ->getFirst(); 43 | 44 | return $bounceAccount; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/RecipientListRepository.php: -------------------------------------------------------------------------------- 1 | get(ConfigurationManager::class); 22 | $storagePid = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'newsletter', 'storagePid'); 23 | 24 | if ($storagePid['storagePid']) { 25 | $query->getQuerySettings()->setRespectStoragePage(true); 26 | } 27 | 28 | return $query; 29 | } 30 | 31 | /** 32 | * Returns a RecipientList already initialized, even if it is hidden 33 | * 34 | * @param int $uidRecipientlist 35 | * 36 | * @return RecipientList 37 | */ 38 | public function findByUidInitialized($uidRecipientlist) 39 | { 40 | $query = $this->createQuery(); 41 | $query->getQuerySettings()->setRespectSysLanguage(false); 42 | $query->getQuerySettings()->setRespectStoragePage(false); 43 | $query->getQuerySettings()->setIgnoreEnableFields(true); // because of this line hidden objects can be retrieved 44 | $recipientList = $query->matching( 45 | $query->equals('uid', $uidRecipientlist) 46 | ) 47 | ->execute() 48 | ->getFirst(); 49 | 50 | if ($recipientList) { 51 | $recipientList->init(); 52 | } 53 | 54 | return $recipientList; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Classes/Exception.php: -------------------------------------------------------------------------------- 1 | objectManager = $objectManager; 49 | } 50 | 51 | /** 52 | * Creates an Ext Direct Transaction and adds it to the request instance. 53 | * 54 | * @param string $action The "action" – the "controller object name" in FLOW3 terms 55 | * @param string $method The "method" – the "action name" in FLOW3 terms 56 | * @param array $data Numeric array of arguments which are eventually passed to the FLOW3 action method 57 | * @param mixed $tid The ExtDirect transaction id 58 | */ 59 | public function createAndAddTransaction($action, $method, array $data, $tid) 60 | { 61 | $transaction = $this->objectManager->get(Transaction::class, $this, $action, $method, $data, $tid); 62 | $this->transactions[] = $transaction; 63 | } 64 | 65 | /** 66 | * Getter for transactions. 67 | * 68 | * @return array 69 | */ 70 | public function getTransactions() 71 | { 72 | return $this->transactions; 73 | } 74 | 75 | /** 76 | * Whether this request represents a form post or not. 77 | * 78 | * @return bool 79 | */ 80 | public function isFormPost() 81 | { 82 | return $this->formPost; 83 | } 84 | 85 | /** 86 | * Marks this request as representing a form post or not. 87 | * 88 | * @param bool $formPost 89 | */ 90 | public function setFormPost($formPost) 91 | { 92 | $this->formPost = $formPost; 93 | } 94 | 95 | /** 96 | * Whether this request represents a file upload or not. 97 | * 98 | * @return bool 99 | */ 100 | public function isFileUpload() 101 | { 102 | return $this->fileUpload; 103 | } 104 | 105 | /** 106 | * Marks this request as representing a file upload or not. 107 | * 108 | * @param bool $fileUpload 109 | */ 110 | public function setFileUpload($fileUpload) 111 | { 112 | $this->fileUpload = $fileUpload ? true : false; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Classes/MVC/ExtDirect/RequestBuilder.php: -------------------------------------------------------------------------------- 1 | objectManager = $objectManager; 35 | } 36 | 37 | /** 38 | * Injects the ConfigurationManager 39 | * 40 | * @param ConfigurationManager $configurationManager 41 | */ 42 | public function injectConfigurationManager(ConfigurationManager $configurationManager) 43 | { 44 | $this->configurationManager = $configurationManager; 45 | } 46 | 47 | /** 48 | * Builds an Ext Direct request 49 | * 50 | * @return Request The built request 51 | */ 52 | public function build() 53 | { 54 | $postArguments = $_POST; 55 | if (isset($postArguments['extAction'])) { 56 | throw new Exception('Form Post Request building is not yet implemented.', 1281379502); 57 | } 58 | $request = $this->buildJsonRequest(); 59 | 60 | return $request; 61 | } 62 | 63 | /** 64 | * Builds a Json Ext Direct request by reading the transaction data from 65 | * standard input. 66 | * 67 | * @return Request The Ext Direct request object 68 | */ 69 | protected function buildJsonRequest() 70 | { 71 | $transactionDatas = file_get_contents('php://input'); 72 | 73 | if (($transactionDatas = json_decode($transactionDatas)) === null) { 74 | throw new Exception('The request is not a valid Ext Direct request', 1268490738); 75 | } 76 | 77 | if (!is_array($transactionDatas)) { 78 | $transactionDatas = [$transactionDatas]; 79 | } 80 | 81 | /** @var Request $request */ 82 | $request = $this->objectManager->get(Request::class); 83 | foreach ($transactionDatas as $transactionData) { 84 | $request->createAndAddTransaction( 85 | $transactionData->action, $transactionData->method, is_array($transactionData->data) ? $transactionData->data : [], $transactionData->tid 86 | ); 87 | } 88 | 89 | return $request; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Classes/MVC/ExtDirect/TransactionResponse.php: -------------------------------------------------------------------------------- 1 | result = $result; 36 | } 37 | 38 | /** 39 | * Sette for success. 40 | * 41 | * @param bool $success The success of the called action 42 | */ 43 | public function setSuccess($success) 44 | { 45 | $this->success = $success; 46 | } 47 | 48 | /** 49 | * Returns the result of the transaction. 50 | * 51 | * @return mixed The result 52 | */ 53 | public function getResult() 54 | { 55 | return $this->result; 56 | } 57 | 58 | /** 59 | * Returns the state (success/fail) of the transaction. 60 | * 61 | * @return bool The success 62 | */ 63 | public function getSuccess() 64 | { 65 | return $this->success; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Classes/MVC/View/ExtDirectView.php: -------------------------------------------------------------------------------- 1 | renderArray(); 20 | $this->controllerContext->getResponse()->setResult($result); 21 | $this->controllerContext->getResponse()->setSuccess(true); 22 | } 23 | 24 | /** 25 | * Assigns errors to the view and converts them to a format that Ext JS 26 | * understands. 27 | * 28 | * @param array $errors Errors e.g. from mapping results 29 | */ 30 | public function assignErrors(array $errors) 31 | { 32 | $result = []; 33 | foreach ($errors as $argumentName => $argumentError) { 34 | foreach ($argumentError->getErrors() as $propertyName => $propertyError) { 35 | $message = ''; 36 | foreach ($propertyError->getErrors() as $error) { 37 | $message .= $error->getMessage(); 38 | } 39 | $result[$propertyName] = $message; 40 | } 41 | } 42 | $this->assign('value', [ 43 | 'errors' => $result, 44 | 'success' => false, 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Classes/Task/FetchBounces.php: -------------------------------------------------------------------------------- 1 | get(BounceAccountRepository::class); 40 | $bounceAccountCount = count($bounceAccountRepository->findAll()); 41 | 42 | return LocalizationUtility::translate('task_fetch_bounce_additional_information', 'newsletter', [$bounceAccountCount]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Classes/Task/SendEmails.php: -------------------------------------------------------------------------------- 1 | get(NewsletterRepository::class); 42 | 43 | $newslettersToSend = $newsletterRepository->findAllReadyToSend(); 44 | $newslettersBeingSent = $newsletterRepository->findAllBeingSent(); 45 | $newslettersToSendCount = count($newslettersToSend); 46 | $newslettersBeingSentCount = count($newslettersBeingSent); 47 | 48 | $emailNotSentCount = 0; 49 | foreach ($newslettersToSend as $newsletter) { 50 | $emailNotSentCount += $newsletter->getEmailNotSentCount(); 51 | } 52 | foreach ($newslettersBeingSent as $newsletter) { 53 | $emailNotSentCount += $newsletter->getEmailNotSentCount(); 54 | } 55 | 56 | $emailsPerRound = Tools::confParam('mails_per_round'); 57 | 58 | return LocalizationUtility::translate('task_send_emails_additional_information', 'newsletter', [$emailsPerRound, $emailNotSentCount, $newslettersToSendCount, $newslettersBeingSentCount]); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Classes/Tca/BounceAccountDataProvider.php: -------------------------------------------------------------------------------- 1 | getDecryptedFieldValue($field, $result['databaseRow'][$field]); 26 | } 27 | } 28 | 29 | return $result; 30 | } 31 | 32 | /** 33 | * Returns the decrypted field value if set. 34 | * 35 | * @param mixed $field 36 | * @param mixed $value 37 | * 38 | * @return string 39 | */ 40 | private function getDecryptedFieldValue($field, $value) 41 | { 42 | $default = @$GLOBALS['TCA']['tx_newsletter_domain_model_bounceaccount']['columns'][$field]['config']['default']; 43 | 44 | // Set the value 45 | if (empty($value)) { 46 | if ($default) { 47 | $value = $default; 48 | } 49 | } elseif ($value != $default) { 50 | $value = Tools::decrypt($value); 51 | } 52 | 53 | return $value; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Classes/Tca/BounceAccountTca.php: -------------------------------------------------------------------------------- 1 | '; 27 | $result .= ''; 28 | foreach ($keys as $key) { 29 | $result .= '' . $key . ''; 30 | } 31 | $result .= ''; 32 | 33 | $result .= ''; 34 | foreach ($data as $value) { 35 | $result .= '' . $value . ''; 36 | } 37 | $result .= ''; 38 | $result .= ''; 39 | 40 | return $result; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Classes/Tca/RecipientListTca.php: -------------------------------------------------------------------------------- 1 | get(RecipientListRepository::class); 29 | $recipientList = $recipientListRepository->findByUidInitialized($uid); 30 | 31 | $result .= $recipientList->getExtract(); 32 | } 33 | 34 | return $result; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Classes/Update/Transaction.php: -------------------------------------------------------------------------------- 1 | sql_query('START TRANSACTION;'); 31 | $results = self::transactDBQueries($queries); 32 | if ($results->getErrorMessage()) { 33 | $db->sql_query('ROLLBACK;'); 34 | // Because we rolled back nothing was modified so we can safely reset the integrity state. 35 | $results->resetDataIntegrity(); 36 | 37 | return $results; 38 | } 39 | $db->sql_query('COMMIT;'); 40 | } 41 | 42 | return $results; 43 | } 44 | 45 | /** 46 | * Executes an array of database queries. 47 | * 48 | * @param string[] $queries 49 | * An array of SQL queries 50 | * 51 | * @return TransactionResult 52 | */ 53 | private static function transactDBQueries(array $queries) 54 | { 55 | $results = new TransactionResult(count($queries)); 56 | if (!empty($queries)) { 57 | $db = Tools::getDatabaseConnection(); 58 | foreach ($queries as $query) { 59 | $res = $db->sql_query($query); 60 | $results->appendAffectedDataCount($db->sql_affected_rows()); 61 | $error = $db->sql_error(); 62 | if ($error) { 63 | $results->setErrorMessage($error); 64 | break; 65 | } 66 | $results->stepProcessed(); 67 | } 68 | $db->sql_free_result($res); 69 | } 70 | 71 | return $results; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/AbstractViewHelper.php: -------------------------------------------------------------------------------- 1 | pageRenderer = $this->getPageRenderer(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Be/ModuleContainerViewHelper.php: -------------------------------------------------------------------------------- 1 | 15 | * {namespace newsletter=Ecodev\Newsletter\ViewHelpers} 16 | * your additional viewHelpers inside 17 | * 18 | * 19 | * Output: 20 | * "your module content" wrapped with propper head & body tags. 21 | * Default backend CSS styles and JavaScript will be included 22 | * 23 | * 24 | * {namespace newsletter=Ecodev\Newsletter\ViewHelpers} 25 | * your module content 26 | * 27 | */ 28 | class ModuleContainerViewHelper extends AbstractViewHelper 29 | { 30 | /** 31 | * Don't escape anything because we will render the entire page 32 | */ 33 | protected $escapeOutput = false; 34 | 35 | /** 36 | * Renders start page with template.php and pageTitle. 37 | * 38 | * @param string $pageTitle title tag of the module. Not required by default, as BE modules are shown in a frame 39 | * 40 | * @return string 41 | * @see template 42 | * @see \TYPO3\CMS\Core\Page\PageRenderer 43 | */ 44 | public function render($pageTitle = '') 45 | { 46 | $doc = $this->getDocInstance(); 47 | $this->pageRenderer->backPath = ''; 48 | $this->pageRenderer->loadExtJS(); 49 | 50 | // From TYPO3 8.6.0 onward t3skin is located in core (see: https://forge.typo3.org/issues/79259). 51 | if (version_compare(TYPO3_version, '8.6.0', '>=')) { 52 | $this->pageRenderer->addCssFile('sysext/core/Resources/Public/ExtJs/xtheme-t3skin.css'); 53 | } else { 54 | // Anything before 8.6.0 must still use the old t3skin EXT path. 55 | $this->pageRenderer->addCssFile('sysext/t3skin/extjs/xtheme-t3skin.css'); 56 | } 57 | 58 | $this->renderChildren(); 59 | 60 | $this->pageRenderer->enableCompressJavaScript(); 61 | $this->pageRenderer->enableCompressCss(); 62 | $this->pageRenderer->enableConcatenateFiles(); 63 | 64 | $output = $doc->startPage($pageTitle); 65 | $output .= $this->pageRenderer->getBodyContent(); 66 | $output .= $doc->endPage(); 67 | 68 | return $output; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/ConfigurationViewHelper.php: -------------------------------------------------------------------------------- 1 | pageRenderer->addJsInlineCode('Ext.ux.Ecodev.Newsletter.Configuration', $javascript); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/CsvValuesViewHelper.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * 17 | */ 18 | class ExtDirectProviderViewHelper extends AbstractViewHelper 19 | { 20 | /** 21 | * @var Api 22 | */ 23 | protected $apiService; 24 | 25 | /** 26 | * @see Classes/Core/ViewHelper/\TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper#initializeArguments() 27 | */ 28 | public function initializeArguments() 29 | { 30 | $objectManager = GeneralUtility::makeInstance(ObjectManager::class); 31 | $this->apiService = $objectManager->get(Api::class); 32 | } 33 | 34 | /** 35 | * Generates a Ext.Direct API descriptor and adds it to the pagerenderer. 36 | * Also calls Ext.Direct.addProvider() on itself (at js side). 37 | * The remote API is directly useable. 38 | * 39 | * @param string $name the name for the javascript variable 40 | * @param string $namespace the namespace the variable is placed 41 | * @param string $routeUrl you can specify a URL that acts as router 42 | */ 43 | public function render($name = 'remoteDescriptor', $namespace = 'Ext.ux.Ecodev.Newsletter.Remote', $routeUrl = null) 44 | { 45 | if ($routeUrl === null) { 46 | $routeUrl = $this->controllerContext->getUriBuilder()->reset()->build() . '&Ecodev\\Newsletter\\ExtDirectRequest=1'; 47 | } 48 | 49 | $api = $this->apiService->createApi($routeUrl, $namespace); 50 | 51 | // prepare output variable 52 | $jsCode = ''; 53 | $descriptor = $namespace . '.' . $name; 54 | // build up the output 55 | $jsCode .= 'Ext.ns(\'' . $namespace . '\'); ' . "\n"; 56 | $jsCode .= $descriptor . ' = '; 57 | $jsCode .= json_encode($api); 58 | $jsCode .= ";\n"; 59 | $jsCode .= 'Ext.Direct.addProvider(' . $descriptor . ');' . "\n"; 60 | // add the output to the pageRenderer 61 | $this->pageRenderer->addExtOnReadyCode($jsCode, true); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/IncludeCssFileViewHelper.php: -------------------------------------------------------------------------------- 1 | -Tags 11 | * 12 | * = Examples = 13 | * 14 | * 15 | * 16 | * 17 | */ 18 | class IncludeCssFileViewHelper extends AbstractViewHelper 19 | { 20 | /** 21 | * Calls addCssFile on the Instance of TYPO3\CMS\Core\Page\PageRenderer. 22 | * 23 | * @param string $name the file to include 24 | * @param string $extKey the extension, where the file is located 25 | * @param string $pathInsideExt the path to the file relative to the ext-folder 26 | * 27 | * @return string the link 28 | */ 29 | public function render($name = null, $extKey = null, $pathInsideExt = 'Resources/Public/Styles/') 30 | { 31 | if ($extKey === null) { 32 | $extKey = $this->controllerContext->getRequest()->getControllerExtensionKey(); 33 | } 34 | 35 | if (TYPO3_MODE === 'FE') { 36 | $extPath = ExtensionManagementUtility::extPath($extKey); 37 | $extRelPath = mb_substr($extPath, mb_strlen(PATH_site)); 38 | } else { 39 | $extRelPath = ExtensionManagementUtility::extRelPath($extKey); 40 | } 41 | 42 | $this->pageRenderer->addCssFile($extRelPath . $pathInsideExt . $name); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/IncludeExtOnReadyFromFileViewHelper.php: -------------------------------------------------------------------------------- 1 | -Tags 12 | * 13 | * = Examples = 14 | * 15 | * 16 | * 17 | * 18 | */ 19 | class IncludeExtOnReadyFromFileViewHelper extends AbstractViewHelper 20 | { 21 | /** 22 | * Calls addJsFile on the Instance of TYPO3\CMS\Core\Page\PageRenderer. 23 | * 24 | * @param string $name the file to include 25 | * @param string $extKey the extension, where the file is located 26 | * @param string $pathInsideExt the path to the file relative to the ext-folder 27 | */ 28 | public function render($name = 'extOnReady.js', $extKey = null, $pathInsideExt = 'Resources/Public/JavaScript/') 29 | { 30 | if ($extKey == null) { 31 | $extKey = $this->controllerContext->getRequest()->getControllerExtensionKey(); 32 | } 33 | $extPath = ExtensionManagementUtility::extPath($extKey); 34 | 35 | $filePath = $extPath . $pathInsideExt . $name; 36 | 37 | if (!file_exists($filePath)) { 38 | throw new Exception('File not found: ' . $filePath, 1264197781); 39 | } 40 | 41 | $fileContent = file_get_contents($extPath . $pathInsideExt . $name); 42 | 43 | $this->pageRenderer->addExtOnReadyCode($fileContent); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/IncludeJsFileViewHelper.php: -------------------------------------------------------------------------------- 1 | -Tags 11 | * 12 | * = Examples = 13 | * 14 | * 15 | * 16 | * 17 | */ 18 | class IncludeJsFileViewHelper extends AbstractViewHelper 19 | { 20 | /** 21 | * Calls addJsFile on the Instance of TYPO3\CMS\Core\Page\PageRenderer. 22 | * 23 | * @param string $name the file to include 24 | * @param string $extKey the extension, where the file is located 25 | * @param string $pathInsideExt the path to the file relative to the ext-folder 26 | */ 27 | public function render($name = null, $extKey = null, $pathInsideExt = 'Resources/Public/JavaScript/') 28 | { 29 | if ($extKey == null) { 30 | $extKey = $this->controllerContext->getRequest()->getControllerExtensionKey(); 31 | } 32 | if (TYPO3_MODE === 'FE') { 33 | $extPath = ExtensionManagementUtility::extPath($extKey); 34 | $extRelPath = mb_substr($extPath, mb_strlen(PATH_site)); 35 | } else { 36 | $extRelPath = ExtensionManagementUtility::extRelPath($extKey); 37 | } 38 | $this->pageRenderer->addJsFile($extRelPath . $pathInsideExt . $name); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/IncludeJsFolderViewHelper.php: -------------------------------------------------------------------------------- 1 | -Tags 12 | * 13 | * = Examples = 14 | * 15 | * 16 | * 17 | * 18 | */ 19 | class IncludeJsFolderViewHelper extends AbstractViewHelper 20 | { 21 | /** 22 | * Calls addJsFile for each file in the given folder on the Instance of TYPO3\CMS\Core\Page\PageRenderer. 23 | * 24 | * @param string $name the file to include 25 | * @param string $extKey the extension, where the file is located 26 | * @param string $pathInsideExt the path to the file relative to the ext-folder 27 | * @param bool $recursive 28 | */ 29 | public function render($name = null, $extKey = null, $pathInsideExt = 'Resources/Public/JavaScript/', $recursive = false) 30 | { 31 | if ($extKey == null) { 32 | $extKey = $this->controllerContext->getRequest()->getControllerExtensionKey(); 33 | } 34 | $extPath = ExtensionManagementUtility::extPath($extKey); 35 | if (TYPO3_MODE === 'FE') { 36 | $extRelPath = mb_substr($extPath, mb_strlen(PATH_site)); 37 | } else { 38 | $extRelPath = ExtensionManagementUtility::extRelPath($extKey); 39 | } 40 | $absFolderPath = $extPath . $pathInsideExt . $name; 41 | // $files will include all files relative to $pathInsideExt 42 | if ($recursive === false) { 43 | $files = GeneralUtility::getFilesInDir($absFolderPath); 44 | foreach ($files as $hash => $filename) { 45 | $files[$hash] = $name . $filename; 46 | } 47 | } else { 48 | $files = GeneralUtility::getAllFilesAndFoldersInPath([], $absFolderPath, '', 0, 99, '\\.svn'); 49 | foreach ($files as $hash => $absPath) { 50 | $files[$hash] = str_replace($extPath . $pathInsideExt, '', $absPath); 51 | } 52 | } 53 | foreach ($files as $name) { 54 | $this->pageRenderer->addJsFile($extRelPath . $pathInsideExt . $name); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/IncludeModuleBodyViewHelper.php: -------------------------------------------------------------------------------- 1 | -Tags 9 | * 10 | * = Examples = 11 | * 12 | * 13 | * 14 | * 15 | */ 16 | class IncludeModuleBodyViewHelper extends AbstractViewHelper 17 | { 18 | /** 19 | * Calls addJsFile on the Instance of TYPO3\CMS\Core\Page\PageRenderer. 20 | */ 21 | public function render() 22 | { 23 | $content = $this->renderChildren(); 24 | $this->pageRenderer->addBodyContent($content); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/LocalizationViewHelper.php: -------------------------------------------------------------------------------- 1 | controllerContext->getRequest()->getControllerExtensionKey(); 25 | } 26 | $extPath = ExtensionManagementUtility::extPath($extKey); 27 | 28 | $localizations = []; 29 | foreach ($names as $name) { 30 | $filePath = $extPath . $pathInsideExt . $name; 31 | $localizations = array_merge($localizations, $this->getLocalizations($filePath)); 32 | } 33 | 34 | $localizations = json_encode($localizations); 35 | $javascript = "Ext.ux.Ecodev.Newsletter.Language = $localizations;"; 36 | 37 | $this->pageRenderer->addJsInlineCode($filePath, $javascript); 38 | } 39 | 40 | /** 41 | * Returns localization variables within an array 42 | * 43 | * @param string $filePath 44 | * 45 | * @throws Exception 46 | * @return array 47 | */ 48 | protected function getLocalizations($filePath) 49 | { 50 | global $LANG; 51 | global $LOCAL_LANG; 52 | 53 | // Language inclusion 54 | $LANG->includeLLFile($filePath); 55 | if (!isset($LOCAL_LANG[$LANG->lang]) || empty($LOCAL_LANG[$LANG->lang])) { 56 | $lang = 'default'; 57 | } else { 58 | $lang = $LANG->lang; 59 | } 60 | 61 | $result = []; 62 | foreach ($LOCAL_LANG[$lang] as $key => $value) { 63 | $target = $value[0]['target']; 64 | 65 | // Replace '.' in key because it would break JSON 66 | $key = str_replace('.', '_', $key); 67 | $result[$key] = $target; 68 | } 69 | 70 | return $result; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Configuration/TCA/tx_newsletter_domain_model_link.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'title' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang_db.xlf:tx_newsletter_domain_model_link', 6 | 'label' => 'url', 7 | 'iconfile' => 'EXT:newsletter/Resources/Public/Icons/tx_newsletter_domain_model_link.svg', 8 | ], 9 | 'interface' => [ 10 | 'showRecordFieldList' => 'url,opened_count,newsletter', 11 | ], 12 | 'types' => [ 13 | '1' => ['showitem' => 'url,opened_count,newsletter'], 14 | ], 15 | 'palettes' => [ 16 | '1' => ['showitem' => ''], 17 | ], 18 | 'columns' => [ 19 | 'url' => [ 20 | 'label' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang_db.xlf:tx_newsletter_domain_model_link.url', 21 | 'config' => [ 22 | 'type' => 'input', 23 | 'size' => 40, 24 | 'eval' => 'trim', 25 | 'readOnly' => true, 26 | ], 27 | ], 28 | 'opened_count' => [ 29 | 'label' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang_db.xlf:tx_newsletter_domain_model_link.opened_count', 30 | 'config' => [ 31 | 'type' => 'input', 32 | 'size' => 4, 33 | 'eval' => 'int', 34 | 'readOnly' => true, 35 | ], 36 | ], 37 | 'newsletter' => [ 38 | 'label' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang_db.xlf:tx_newsletter_domain_model_link.newsletter', 39 | 'config' => [ 40 | 'readOnly' => true, 41 | 'type' => 'inline', 42 | 'foreign_table' => 'tx_newsletter_domain_model_newsletter', 43 | 'minitems' => 0, 44 | 'maxitems' => 1, 45 | 'appearance' => [ 46 | 'collapse' => 0, 47 | 'showSynchronizationLink' => 1, 48 | 'showPossibleLocalizationRecords' => 1, 49 | 'showAllLocalizationLink' => 1, 50 | ], 51 | ], 52 | ], 53 | ], 54 | ]; 55 | -------------------------------------------------------------------------------- /Configuration/TypoScript/constants.txt: -------------------------------------------------------------------------------- 1 | module.tx_newsletter { 2 | view { 3 | # cat=module.tx_newsletter/file; type=string; label=Path to template root (BE) 4 | templateRootPath = EXT:newsletter/Resources/Private/Templates/ 5 | # cat=module.tx_newsletter/file; type=string; label=Path to template partials (BE) 6 | partialRootPath = EXT:newsletter/Resources/Private/Partials/ 7 | # cat=module.tx_newsletter/file; type=string; label=Path to template layouts (BE) 8 | layoutRootPath = EXT:newsletter/Resources/Private/Layouts/ 9 | } 10 | persistence { 11 | # cat=module.tx_newsletter//a; type=string; label=Default storage PID 12 | storagePid = 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Documentation/ChangeLog/Index.rst: -------------------------------------------------------------------------------- 1 | .. ================================================== 2 | .. FOR YOUR INFORMATION 3 | .. -------------------------------------------------- 4 | .. -*- coding: utf-8 -*- with BOM. 5 | 6 | .. include:: ../Includes.txt 7 | 8 | 9 | .. _changelog: 10 | 11 | ChangeLog 12 | ========= 13 | 14 | The changelog is available from each release commit messages, publicly available 15 | on GitHub `release pages`_. 16 | 17 | .. _release pages: https://github.com/Ecodev/newsletter/releases 18 | -------------------------------------------------------------------------------- /Documentation/Developer/Index.rst: -------------------------------------------------------------------------------- 1 | .. ================================================== 2 | .. FOR YOUR INFORMATION 3 | .. -------------------------------------------------- 4 | .. -*- coding: utf-8 -*- with BOM. 5 | 6 | .. include:: ../Includes.txt 7 | 8 | 9 | .. _developer: 10 | 11 | Developer Corner 12 | ================ 13 | 14 | Target group: **Developers** 15 | 16 | This extension is developed on GitHub, if you wish to contribute to the 17 | `project`_ you are most welcome to participate. Be sure to contact the team 18 | before starting to work on significant modifications. 19 | 20 | .. _project: https://github.com/Ecodev/newsletter 21 | 22 | The following figure shows the original model of the extension, while it has 23 | changed overtime, it still gives a good idea of how it the code is structured: 24 | 25 | 26 | .. figure:: ../Images/model.png 27 | :alt: Original model of Newsletter 28 | 29 | .. _developer-hooks: 30 | 31 | Hooks 32 | ----- 33 | 34 | There are only two hooks available: ``substituteMarkersHook`` and 35 | ``getConfiguredMailerHook``. See source code for details. 36 | 37 | 38 | If you need additional hooks, post your request to the 39 | projects `issues`_ page on GitHub with a detailed explanation of your use-case. 40 | 41 | .. _issues: https://github.com/Ecodev/newsletter/issues 42 | 43 | .. _developer-api: 44 | 45 | API 46 | --- 47 | 48 | There is currently no published API for this extension however there is a 49 | Doxyfile configuration in the Documentation folder. This file can be used to 50 | generate source documentation in combination with `Doxygen`_. 51 | 52 | .. _Doxygen: http://www.doxygen.org 53 | -------------------------------------------------------------------------------- /Documentation/Images/Newsletter_-_Sending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/Images/Newsletter_-_Sending.png -------------------------------------------------------------------------------- /Documentation/Images/Newsletter_-_Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/Images/Newsletter_-_Settings.png -------------------------------------------------------------------------------- /Documentation/Images/Newsletter_-_Status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/Images/Newsletter_-_Status.png -------------------------------------------------------------------------------- /Documentation/Images/Overview_-_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/Images/Overview_-_small.png -------------------------------------------------------------------------------- /Documentation/Images/Statistics_-_Emails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/Images/Statistics_-_Emails.png -------------------------------------------------------------------------------- /Documentation/Images/Statistics_-_Links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/Images/Statistics_-_Links.png -------------------------------------------------------------------------------- /Documentation/Images/Statistics_-_Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/Images/Statistics_-_Overview.png -------------------------------------------------------------------------------- /Documentation/Images/model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/Images/model.png -------------------------------------------------------------------------------- /Documentation/Includes.txt: -------------------------------------------------------------------------------- 1 | .. ================================================== 2 | .. FOR YOUR INFORMATION 3 | .. -------------------------------------------------- 4 | .. -*- coding: utf-8 -*- with BOM. 5 | 6 | .. This is 'Includes.txt'. It is included at the very top of each and 7 | every ReST source file in this documentation project (= manual). 8 | 9 | 10 | .. ================================================== 11 | .. DEFINE SOME TEXT ROLES 12 | .. -------------------------------------------------- 13 | 14 | .. role:: typoscript(code) 15 | 16 | .. role:: ts(typoscript) 17 | :class: typoscript 18 | 19 | .. role:: php(code) 20 | 21 | .. highlight:: php 22 | -------------------------------------------------------------------------------- /Documentation/Index.rst: -------------------------------------------------------------------------------- 1 | .. ================================================== 2 | .. FOR YOUR INFORMATION 3 | .. -------------------------------------------------- 4 | .. -*- coding: utf-8 -*- with BOM. 5 | 6 | .. include:: Includes.txt 7 | 8 | .. _start: 9 | 10 | ========== 11 | Newsletter 12 | ========== 13 | 14 | .. only:: html 15 | 16 | :Classification: 17 | newsletter 18 | 19 | :Language: 20 | en 21 | 22 | :Description: 23 | Manual covering TYPO3 extension Newsletter 24 | 25 | :Keywords: 26 | newsletter, email, scheduling, bulk email 27 | 28 | :Copyright: 29 | 2010-2016 30 | 31 | :Author: 32 | Ecodev 33 | 34 | :Email: 35 | contact@ecodev.ch 36 | 37 | :License: 38 | This document is published under the Open Publication License 39 | available from http://www.opencontent.org/openpub/ 40 | 41 | :Support: 42 | If you need help with this extension, commercial support may be obtained 43 | by contacting `ecodev.ch `_. 44 | 45 | :Rendered: 46 | |today| 47 | 48 | The content of this document is related to TYPO3, 49 | a GNU/GPL CMS/Framework available from `www.typo3.org `_. 50 | 51 | 52 | 53 | **Table of Contents** 54 | 55 | .. toctree:: 56 | :maxdepth: 3 57 | :titlesonly: 58 | 59 | Introduction/Index 60 | User/Index 61 | Administrator/Index 62 | Configuration/Index 63 | Configuration/Recipient_List_SQL_Examples 64 | Developer/Index 65 | KnownProblems/Index 66 | ToDoList/Index 67 | ChangeLog/Index 68 | Links 69 | -------------------------------------------------------------------------------- /Documentation/Introduction/Index.rst: -------------------------------------------------------------------------------- 1 | .. ================================================== 2 | .. FOR YOUR INFORMATION 3 | .. -------------------------------------------------- 4 | .. -*- coding: utf-8 -*- with BOM. 5 | 6 | .. include:: ../Includes.txt 7 | 8 | 9 | .. _introduction: 10 | 11 | Introduction 12 | ============ 13 | 14 | 15 | .. _what-it-does: 16 | 17 | What does it do? 18 | ---------------- 19 | 20 | A TYPO3 extension to send any pages as a newsletter to several recipients at 21 | once. 22 | 23 | Originally based on `TC Directmail`_ 2.0.2, 24 | the mailing engine was almost entirely rewritten but most features were 25 | preserved.We now use SwiftMailer (from TYPO3 core). And it aims to improve the 26 | user experience and works out of the box. 27 | 28 | .. _TC Directmail: http://typo3.org/extensions/repository/view/tcdirectmail/current/ 29 | 30 | .. _how-it-compares: 31 | 32 | Comparison with TC Directmail 2.0.2 33 | ----------------------------------- 34 | 35 | What's better 36 | ^^^^^^^^^^^^^ 37 | 38 | - Use of `SwiftMailer`_ 39 | - Brand new database structure allowing for much more size efficient 40 | storage 41 | - Two special markers available: :code:`###newsletter_view_url###` and 42 | :code:`###newsletter_unsubscribe_url###` 43 | - Better cleaning of javascript in email content 44 | - Plain text quality is improved 45 | 46 | What's worse 47 | ^^^^^^^^^^^^ 48 | 49 | - Removed wizard for recipientlist generation 50 | - Only one recipient list per newsletter (workaround: send multiple 51 | newsletter or UNION via raw sql) 52 | 53 | .. _SwiftMailer: http://swiftmailer.org/ 54 | 55 | .. _screenshots: 56 | 57 | Screenshots 58 | ----------- 59 | 60 | Current status of newsletter 61 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 62 | 63 | .. figure:: ../Images/Newsletter_-_Status.png 64 | :alt: Status of Newsletter 65 | 66 | Settings for newsletter 67 | ^^^^^^^^^^^^^^^^^^^^^^^ 68 | 69 | .. figure:: ../Images/Newsletter_-_Settings.png 70 | :alt: Settings for Newsletter 71 | 72 | Newsletter planning (and testing) 73 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 74 | 75 | .. figure:: ../Images/Newsletter_-_Sending.png 76 | :alt: Newsletter planning and testing 77 | 78 | Statistics overview with charts for one newsletter 79 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 80 | 81 | .. figure:: ../Images/Statistics_-_Overview.png 82 | :alt: Statistics overview for one newsletter 83 | 84 | Statistics of all emails for one newsletter 85 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 86 | 87 | .. figure:: Images/Statistics_-_Emails.png 88 | :alt: Statistics of all emails for one newsletter 89 | 90 | Statistics of all links for one newsletter 91 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 92 | 93 | .. figure:: ../Images/Statistics_-_Links.png 94 | :alt: Statistics of all links for one newsletter 95 | -------------------------------------------------------------------------------- /Documentation/Links.rst: -------------------------------------------------------------------------------- 1 | .. ================================================== 2 | .. FOR YOUR INFORMATION 3 | .. -------------------------------------------------- 4 | .. -*- coding: utf-8 -*- with BOM. 5 | 6 | .. include:: Includes.txt 7 | 8 | 9 | .. _links: 10 | 11 | Links 12 | ----- 13 | 14 | :TER: 15 | https://typo3.org/extensions/repository/view/newsletter 16 | 17 | :Bug Tracker: 18 | https://github.com/Ecodev/newsletter/issues 19 | 20 | :Git Repository: 21 | https://github.com/Ecodev/newsletter 22 | 23 | :Contact: 24 | If you need help with this extension, commercial support may be obtained 25 | by contacting `ecodev.ch `_. 26 | -------------------------------------------------------------------------------- /Documentation/Settings.yml: -------------------------------------------------------------------------------- 1 | # This is the project specific Settings.yml file. 2 | # Place Sphinx specific build information here. 3 | # Settings given here will replace the settings of 'conf.py'. 4 | 5 | # Below is an example of intersphinx mapping declaration 6 | # Add more mappings depending on what manual you want to link to 7 | # Remove entirely if you don't need cross-linking 8 | 9 | --- 10 | conf.py: 11 | copyright: 2010-2016 12 | project: Newsletter 13 | intersphinx_mapping: 14 | t3tsref: 15 | - https://docs.typo3.org/typo3cms/TyposcriptReference/ 16 | - null 17 | latex_documents: 18 | - - Index 19 | - newsletter.tex 20 | - Newsletter 21 | - Ecodev 22 | - manual 23 | latex_elements: 24 | papersize: a4paper 25 | pointsize: 10pt 26 | preamble: \usepackage{typo3} 27 | html_theme_options: 28 | github_repository: Ecodev/Newsletter 29 | github_branch: latest 30 | ... 31 | -------------------------------------------------------------------------------- /Documentation/gui mockup/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/background.png -------------------------------------------------------------------------------- /Documentation/gui mockup/background_usertools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/background_usertools.png -------------------------------------------------------------------------------- /Documentation/gui mockup/bullet_toggle_minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/bullet_toggle_minus.png -------------------------------------------------------------------------------- /Documentation/gui mockup/bullet_toggle_plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/bullet_toggle_plus.png -------------------------------------------------------------------------------- /Documentation/gui mockup/calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/calendar.png -------------------------------------------------------------------------------- /Documentation/gui mockup/chart_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/chart_bar.png -------------------------------------------------------------------------------- /Documentation/gui mockup/chart_curve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/chart_curve.png -------------------------------------------------------------------------------- /Documentation/gui mockup/chart_pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/chart_pie.png -------------------------------------------------------------------------------- /Documentation/gui mockup/diagnostic-linkcheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/diagnostic-linkcheck.png -------------------------------------------------------------------------------- /Documentation/gui mockup/diagnostic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/diagnostic.png -------------------------------------------------------------------------------- /Documentation/gui mockup/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/eye.png -------------------------------------------------------------------------------- /Documentation/gui mockup/general-stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/general-stats.png -------------------------------------------------------------------------------- /Documentation/gui mockup/piechart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/piechart.png -------------------------------------------------------------------------------- /Documentation/gui mockup/stats-click-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/stats-click-graph.png -------------------------------------------------------------------------------- /Documentation/gui mockup/stats-general-graph-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/stats-general-graph-header.png -------------------------------------------------------------------------------- /Documentation/gui mockup/stats-general-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/stats-general-graph.png -------------------------------------------------------------------------------- /Documentation/gui mockup/stats-general.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Documentation/gui mockup/stats-general.png -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | DEPRECATED 2 | ========== 3 | 4 | This extension is **no longer maintained** and will never support TYPO3 9+ for the reasons explained in https://github.com/Ecodev/newsletter/issues/168. 5 | 6 | Users should migrate to another extension, or fork this project. 7 | 8 | Newsletter 9 | ========== 10 | 11 | |badge_travis| |badge_scrutinizer| |badge_coverage| |badge_downloads| |badge_version| |badge_license| |badge_gitter| 12 | 13 | .. |badge_travis| image:: https://travis-ci.org/Ecodev/newsletter.svg?branch=master 14 | :target: https://travis-ci.org/Ecodev/newsletter 15 | 16 | .. |badge_scrutinizer| image:: https://scrutinizer-ci.com/g/Ecodev/newsletter/badges/quality-score.png?b=master 17 | :target: https://scrutinizer-ci.com/g/Ecodev/newsletter 18 | 19 | .. |badge_coverage| image:: https://scrutinizer-ci.com/g/Ecodev/newsletter/badges/coverage.png?b=master 20 | :target: https://scrutinizer-ci.com/g/Ecodev/newsletter 21 | 22 | .. |badge_downloads| image:: https://poser.pugx.org/ecodev/newsletter/downloads 23 | :alt: Total Downloads 24 | :target: https://packagist.org/packages/ecodev/newsletter 25 | 26 | .. |badge_version| image:: https://poser.pugx.org/ecodev/newsletter/v/stable 27 | :alt: Latest Stable Version 28 | :target: https://packagist.org/packages/ecodev/newsletter 29 | 30 | .. |badge_license| image:: https://poser.pugx.org/ecodev/newsletter/license 31 | :alt: License 32 | :target: https://packagist.org/packages/ecodev/newsletter 33 | 34 | .. |badge_gitter| image:: https://badges.gitter.im/Ecodev/newsletter.svg 35 | :alt: Join the chat at https://gitter.im/Ecodev/newsletter 36 | :target: https://gitter.im/Ecodev/newsletter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 37 | 38 | .. image:: Documentation/Images/Overview_-_small.png 39 | :align: right 40 | 41 | 42 | A TYPO3 extension to send any pages as a newsletter to several recipients at once. 43 | 44 | Originally based on `TC Directmail`_ 2.0.2, 45 | the mailing engine was almost entirely rewritten but most features were preserved. 46 | We now use SwiftMailer (from TYPO3 core). And it aims to improve the user 47 | experience and works out of the box. 48 | 49 | 50 | Read more in ``Documentation`` folder. 51 | 52 | .. _TC Directmail: http://typo3.org/extensions/repository/view/tcdirectmail/current/ 53 | 54 | -------------------------------------------------------------------------------- /Resources/Private/.htaccess: -------------------------------------------------------------------------------- 1 | deny from all -------------------------------------------------------------------------------- /Resources/Private/Language/de.locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | email 8 | E-Mail 9 | 10 | 11 | server 12 | Server 13 | 14 | 15 | protocol 16 | Protokoll 17 | 18 | 19 | username 20 | Benutzername 21 | 22 | 23 | password 24 | Kennwort 25 | 26 | 27 | port 28 | Port 29 | 30 | 31 | fetchmail config 32 | fetchmail Konfigurations 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Resources/Private/Language/de.locallang_csh_tx_newsletter_domain_model_email.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | startTime 8 | Start Zeit 9 | 10 | 11 | endTime 12 | End Zeit 13 | 14 | 15 | recipientAddress 16 | Empfänger Adresse 17 | 18 | 19 | recipientData 20 | Empfänger Daten 21 | 22 | 23 | opened 24 | geöffnet 25 | 26 | 27 | bounced 28 | zurückgekommen 29 | 30 | 31 | newsletter 32 | Newsletter 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Resources/Private/Language/de.locallang_csh_tx_newsletter_domain_model_link.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | url 8 | url 9 | 10 | 11 | opened 12 | geöffnet 13 | 14 | 15 | email 16 | email 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Resources/Private/Language/de.locallang_module.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | Newsletter 8 | Newsletter 9 | 10 | 11 | Newsletter 12 | Newsletter 13 | 14 | 15 | Newsletter control module. This module allows you to control the newsletter settings. 16 | Newsletter Modul. Mit diesem Modul kann der Newsletter gesteuert und eingerichtet werden. 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Resources/Private/Language/fr.locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | email 8 | Adresse email du compte de rebont qui sera inclut dans le champ "reply-to" des emails envoyés 9 | 10 | 11 | server 12 | Nom d'hôte du serveur email sur lequel récupéré les emails rebondit 13 | 14 | 15 | protocol 16 | Protocole du serveur email 17 | 18 | 19 | username 20 | Nom d'utilistateur du serveur email 21 | 22 | 23 | password 24 | Mot de passe du serveur email 25 | 26 | 27 | port 28 | Port du serveur email 29 | 30 | 31 | fetchmail config 32 | Configuration fetchmail 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Resources/Private/Language/fr.locallang_csh_tx_newsletter_domain_model_email.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | startTime 8 | Date de début d'envoi de l'email 9 | 10 | 11 | endTime 12 | Date de fin d'envoi de l'email 13 | 14 | 15 | recipientAddress 16 | Adresse email du destinataire 17 | 18 | 19 | recipientData 20 | Donnée du destinataire qui peuvent être utilisée dans le corp de l'email via des markers 21 | 22 | 23 | opened 24 | Si l'email a été ouvert 25 | 26 | 27 | bounced 28 | Si l'email a rebondit 29 | 30 | 31 | newsletter 32 | Newsletter à qui appartient cet email 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Resources/Private/Language/fr.locallang_csh_tx_newsletter_domain_model_link.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | url 8 | URL 9 | 10 | 11 | opened 12 | Nombre de fois que le lien a été cliqué 13 | 14 | 15 | email 16 | Newsletter à qui appartient ce lien 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Resources/Private/Language/fr.locallang_module.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | Newsletter 8 | Newsletter 9 | 10 | 11 | Newsletter 12 | Newsletter 13 | 14 | 15 | Newsletter control module. This module allows you to control the newsletter settings. 16 | Module de Newsletter. Ce module permet d'envoyer des pages en tant que Newsletter et de voir les statistiques d'envoi. 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | email 8 | 9 | 10 | server 11 | 12 | 13 | protocol 14 | 15 | 16 | username 17 | 18 | 19 | password 20 | 21 | 22 | port 23 | 24 | 25 | fetchmail config 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_email.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | startTime 8 | 9 | 10 | endTime 11 | 12 | 13 | recipientAddress 14 | 15 | 16 | recipientData 17 | 18 | 19 | opened 20 | 21 | 22 | bounced 23 | 24 | 25 | newsletter 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_link.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | url 8 | 9 | 10 | opened 11 | 12 | 13 | email 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_newsletter.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | When the newsletter will start sending emails 8 | 9 | 10 | beginTime 11 | 12 | 13 | endTime 14 | 15 | 16 | 0-7 values to indicates when this newsletter will repeat 17 | 18 | 19 | Tool used to convert to plain text 20 | 21 | 22 | Whether this newsletter is for test purpose. If it is it will be ignored in statistics 23 | 24 | 25 | List of files to be attached (comma separated list 26 | 27 | 28 | The name of the newsletter sender 29 | 30 | 31 | The email of the newsletter sender 32 | 33 | 34 | The name of the newsletter Reply-to: 35 | 36 | 37 | The email of the newsletter Reply-to: 38 | 39 | 40 | injectOpenSpy 41 | 42 | 43 | injectLinksSpy 44 | 45 | 46 | bounceAccount 47 | 48 | 49 | recipientList 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_recipientlist.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/Private/Language/locallang_module.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | Newsletter 8 | 9 | 10 | Newsletter 11 | 12 | 13 | Newsletter control module. This module allows you to control the newsletter settings. 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Email/Show.html: -------------------------------------------------------------------------------- 1 | {namespace newsletter=Ecodev\Newsletter\ViewHelpers} 2 | 3 | 4 | {content} 5 | 6 | 7 |

8 | 9 |

10 |
11 |
12 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Email/Unsubscribe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Unsubscribed {recipientAddress} successfully.

4 |
5 | 6 |

7 | 8 |

9 |
10 |
11 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Module/Index.html: -------------------------------------------------------------------------------- 1 | {namespace newsletter=Ecodev\Newsletter\ViewHelpers} 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 | 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /Resources/Private/Templates/RecipientList/Export.csv: -------------------------------------------------------------------------------- 1 | {namespace newsletter=Ecodev\Newsletter\ViewHelpers} 2 | 3 | -------------------------------------------------------------------------------- /Resources/Private/Templates/RecipientList/Export.html: -------------------------------------------------------------------------------- 1 | unsuported format 2 | -------------------------------------------------------------------------------- /Resources/Private/Templates/RecipientList/Export.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <{field}>{value} 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Resources/Private/clear.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/newsletter/093f52a1c10c0cf2c20f41f17c6092c58a34d133/Resources/Private/clear.gif -------------------------------------------------------------------------------- /Resources/Public/Icons/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/information.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/ok.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/pie_chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/test-können 한국어 ✓ € ☺ ☹.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/tx_newsletter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/tx_newsletter_domain_model_bounceaccount.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/tx_newsletter_domain_model_email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/tx_newsletter_domain_model_link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/tx_newsletter_domain_model_newsletter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/tx_newsletter_domain_model_recipientlist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Icons/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/DirectFlashMessageDispatcher.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | Ext.ns('Ext.ux.Ecodev.Newsletter'); 5 | /** 6 | * This class fetches Ext.Direct events, fires a event if 7 | * new FlashMessages are available and removes the messages from the 8 | * Ext.Direct response event. 9 | * 10 | * Register your FlashMessage-processing ExtJS component like this: 11 | * Ext.ux.Ecodev.Newsletter.DirectFlashMessages.on('new',function(flashMessages) { 12 | * //do something with incoming FlashMessages 13 | * }); 14 | * 15 | */ 16 | Ext.ux.Ecodev.Newsletter.DirectFlashMessageDispatcher = (function () { 17 | /** 18 | * @class Ext.util.Observable 19 | */ 20 | var directFlashMessages = new Ext.util.Observable(); 21 | directFlashMessages.addEvents('new'); 22 | 23 | var fetchRemoteMessages = function (event) { 24 | if (event.result && event.result.flashMessages) { 25 | var flashMessages = event.result.flashMessages; 26 | delete event.result.flashMessages; 27 | directFlashMessages.fireEvent('new', flashMessages); 28 | } 29 | }; 30 | 31 | var initialize = function () { 32 | Ext.Direct.on('event', fetchRemoteMessages); 33 | }; 34 | 35 | return Ext.apply(directFlashMessages, { 36 | initialize: initialize, 37 | addMessage: function (message) { 38 | this.fireEvent('new', [message]); 39 | }, 40 | addMessages: function (messages) { 41 | this.fireEvent('new', messages); 42 | }, 43 | }); 44 | }()); 45 | }()); 46 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/ExtOnReady.js: -------------------------------------------------------------------------------- 1 | Ext.ux.Ecodev.Newsletter.Module.Application.bootstrap(); 2 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Override/DirectProxyPatch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Patch the DirectProxy to accept paramOrders for store delete, update and create requests. 3 | */ 4 | Ext.override(Ext.data.DirectProxy, { 5 | doRequest: function (action, rs, params, reader, callback, scope, options) { 6 | var args = [], 7 | directFn = this.api[action] || this.directFn; 8 | 9 | if (Ext.isObject(this.paramOrder)) { 10 | paramOrder = this.paramOrder[action]; 11 | if (typeof paramOrder == 'string') { 12 | paramOrder = this.paramOrder.split(/[\s,|]/); 13 | } 14 | } else { 15 | paramOrder = this.paramOrder; 16 | } 17 | 18 | paramsCreateUpdateDestroy = params.jsonData; 19 | 20 | switch (action) { 21 | case Ext.data.Api.actions.create: 22 | if (directFn.directCfg.method.len > 0) { 23 | if (paramOrder) { 24 | for (var i = 0, len = paramOrder.length; i < len; i++) { 25 | args.push(paramsCreateUpdateDestroy[paramOrder[i]]); 26 | } 27 | } 28 | } 29 | break; 30 | case Ext.data.Api.actions.read: 31 | // If the method has no parameters, ignore the paramOrder/paramsAsHash. 32 | if (directFn.directCfg.method.len > 0) { 33 | if (paramOrder) { 34 | for (var i = 0, len = paramOrder.length; i < len; i++) { 35 | args.push(params[paramOrder[i]]); 36 | } 37 | } else if (this.paramsAsHash) { 38 | args.push(params); 39 | } 40 | } 41 | break; 42 | case Ext.data.Api.actions.update: 43 | if (directFn.directCfg.method.len > 0) { 44 | if (paramOrder) { 45 | for (var i = 0, len = paramOrder.length; i < len; i++) { 46 | args.push(paramsCreateUpdateDestroy[paramOrder[i]]); 47 | } 48 | } 49 | } 50 | break; 51 | case Ext.data.Api.actions.destroy: 52 | if (directFn.directCfg.method.len > 0) { 53 | if (paramOrder) { 54 | for (var i = 0, len = paramOrder.length; i < len; i++) { 55 | args.push(paramsCreateUpdateDestroy[paramOrder[i]]); 56 | } 57 | } 58 | } 59 | break; 60 | } 61 | 62 | var trans = { 63 | params: params || {}, 64 | request: { 65 | callback: callback, 66 | scope: scope, 67 | arg: options, 68 | }, 69 | reader: reader, 70 | }; 71 | 72 | args.push(this.createCallback(action, rs, trans), this); 73 | directFn.apply(window, args); 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Statistics/Statistics.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Statistics'); 5 | 6 | /** 7 | * @class Ext.ux.Ecodev.Newsletter.Statistics.Statistics 8 | * @namespace Ext.ux.Ecodev.Newsletter.Statistics 9 | * @extends Ext.Container 10 | * 11 | * Class for statistic container 12 | */ 13 | Ext.ux.Ecodev.Newsletter.Statistics.Statistics = Ext.extend(Ext.Container, { 14 | initComponent: function () { 15 | var config = { 16 | layout: 'border', 17 | title: Ext.ux.Ecodev.Newsletter.Language.statistics_tab, 18 | items: [ 19 | { 20 | split: true, 21 | region: 'north', 22 | xtype: 'Ext.ux.Ecodev.Newsletter.Statistics.NewsletterListMenu', 23 | ref: 'newsletterListMenu', 24 | }, 25 | { 26 | region: 'center', 27 | xtype: 'Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel', 28 | ref: 'statisticsPanel', 29 | }, 30 | ], 31 | }; 32 | Ext.apply(this, config); 33 | Ext.ux.Ecodev.Newsletter.Statistics.Statistics.superclass.initComponent.call(this); 34 | }, 35 | }); 36 | 37 | Ext.reg('Ext.ux.Ecodev.Newsletter.Statistics.Statistics', Ext.ux.Ecodev.Newsletter.Statistics.Statistics); 38 | }()); 39 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Statistics/StatisticsPanel.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Statistics'); 5 | 6 | /** 7 | * @class Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel 8 | * @namespace Ext.ux.Ecodev.Newsletter.Statistics 9 | * @extends Ext.TabPanel 10 | * 11 | * Class for statistic tab panel 12 | */ 13 | Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel = Ext.extend(Ext.TabPanel, { 14 | initComponent: function () { 15 | 16 | var config = { 17 | activeTab: 0, 18 | border: false, 19 | items: [ 20 | { 21 | title: Ext.ux.Ecodev.Newsletter.Language.overview_tab, 22 | xtype: 'Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel.OverviewTab', 23 | itemId: 'overviewTab', 24 | }, 25 | { 26 | title: Ext.ux.Ecodev.Newsletter.Language.emails_tab, 27 | xtype: 'Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel.EmailTab', 28 | itemId: 'emailTab', 29 | }, 30 | { 31 | title: Ext.ux.Ecodev.Newsletter.Language.links_tab, 32 | xtype: 'Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel.LinkTab', 33 | itemId: 'linkTab', 34 | }, 35 | ], 36 | }; 37 | Ext.apply(this, config); 38 | Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel.superclass.initComponent.call(this); 39 | }, 40 | 41 | }); 42 | 43 | Ext.reg('Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel', Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel); 44 | }()); 45 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Store/BounceAccount.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Store'); 5 | 6 | /** 7 | * A Store for the bounceAccount model using ExtDirect to communicate with the 8 | * server side extbase framework. 9 | */ 10 | Ext.ux.Ecodev.Newsletter.Store.BounceAccount = (function () { 11 | 12 | var bounceAccountStore = null; 13 | 14 | var initialize = function () { 15 | if (bounceAccountStore === null) { 16 | bounceAccountStore = new Ext.data.DirectStore({ 17 | storeId: 'Ecodev\\Newsletter\\Domain\\Model\\BounceAccount', 18 | reader: new Ext.data.JsonReader({ 19 | totalProperty: 'total', 20 | successProperty: 'success', 21 | idProperty: '__identity', 22 | root: 'data', 23 | fields: [ 24 | {name: '__identity', type: 'int'}, 25 | {name: 'email', type: 'string'}, 26 | {name: 'server', type: 'string'}, 27 | {name: 'protocol', type: 'string'}, 28 | {name: 'username', type: 'string'}, 29 | { 30 | name: 'fullName', convert: function (v, bounceAccount) { 31 | return String.format('{0} ({1}://{2}@{3})', bounceAccount.email, bounceAccount.protocol, bounceAccount.username, bounceAccount.server); 32 | }, 33 | }, 34 | ], 35 | }), 36 | writer: new Ext.data.JsonWriter({ 37 | encode: false, 38 | writeAllFields: false, 39 | }), 40 | api: { 41 | read: Ext.ux.Ecodev.Newsletter.Remote.BounceAccountController.listAction, 42 | update: Ext.ux.Ecodev.Newsletter.Remote.BounceAccountController.updateAction, 43 | destroy: Ext.ux.Ecodev.Newsletter.Remote.BounceAccountController.destroyAction, 44 | create: Ext.ux.Ecodev.Newsletter.Remote.BounceAccountController.createAction, 45 | }, 46 | paramOrder: { 47 | read: [], 48 | update: ['data'], 49 | create: ['data'], 50 | destroy: ['data'], 51 | }, 52 | autoLoad: true, 53 | 54 | }); 55 | } 56 | }; 57 | 58 | /** 59 | * Public API of this singleton. 60 | */ 61 | return { 62 | initialize: initialize, 63 | }; 64 | }()); 65 | }()); 66 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Store/Email.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Store'); 5 | 6 | /** 7 | * A Store for the email model using ExtDirect to communicate with the 8 | * server side extbase framework. 9 | */ 10 | Ext.ux.Ecodev.Newsletter.Store.Email = (function () { 11 | 12 | var emailStore = null; 13 | 14 | var initialize = function () { 15 | if (emailStore === null) { 16 | emailStore = new Ext.data.DirectStore({ 17 | storeId: 'Ecodev\\Newsletter\\Domain\\Model\\Email', 18 | reader: new Ext.data.JsonReader({ 19 | totalProperty: 'total', 20 | successProperty: 'success', 21 | idProperty: '__identity', 22 | root: 'data', 23 | fields: [ 24 | {name: '__identity', type: 'int'}, 25 | {name: 'recipientAddress', type: 'string'}, 26 | {name: 'beginTime', type: 'date'}, 27 | {name: 'endTime', type: 'date'}, 28 | {name: 'openTime', type: 'date'}, 29 | {name: 'bounceTime', type: 'date'}, 30 | {name: 'authCode', type: 'string'}, 31 | {name: 'recipientAddress', type: 'string'}, 32 | {name: 'unsubscribed', type: 'boolean'}, 33 | ], 34 | }), 35 | writer: new Ext.data.JsonWriter({ 36 | encode: false, 37 | writeAllFields: false, 38 | }), 39 | api: { 40 | read: Ext.ux.Ecodev.Newsletter.Remote.EmailController.listAction, 41 | update: Ext.ux.Ecodev.Newsletter.Remote.EmailController.updateAction, 42 | destroy: Ext.ux.Ecodev.Newsletter.Remote.EmailController.destroyAction, 43 | create: Ext.ux.Ecodev.Newsletter.Remote.EmailController.createAction, 44 | }, 45 | paramOrder: { 46 | read: ['data', 'start', 'limit'], 47 | }, 48 | }); 49 | } 50 | }; 51 | 52 | /** 53 | * Public API of this singleton. 54 | */ 55 | return { 56 | initialize: initialize, 57 | }; 58 | }()); 59 | }()); 60 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Store/Link.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Store'); 5 | 6 | /** 7 | * A Store for the link model using ExtDirect to communicate with the 8 | * server side extbase framework. 9 | */ 10 | Ext.ux.Ecodev.Newsletter.Store.Link = (function () { 11 | 12 | var linkStore = null; 13 | 14 | var initialize = function () { 15 | if (linkStore === null) { 16 | linkStore = new Ext.data.DirectStore({ 17 | storeId: 'Ecodev\\Newsletter\\Domain\\Model\\Link', 18 | reader: new Ext.data.JsonReader({ 19 | totalProperty: 'total', 20 | successProperty: 'success', 21 | idProperty: '__identity', 22 | root: 'data', 23 | fields: [ 24 | {name: '__identity', type: 'int'}, 25 | {name: 'url', type: 'string'}, 26 | {name: 'openedCount', type: 'int'}, 27 | {name: 'openedPercentage', type: 'int'}, 28 | ], 29 | }), 30 | writer: new Ext.data.JsonWriter({ 31 | encode: false, 32 | writeAllFields: false, 33 | }), 34 | api: { 35 | read: Ext.ux.Ecodev.Newsletter.Remote.LinkController.listAction, 36 | update: Ext.ux.Ecodev.Newsletter.Remote.LinkController.updateAction, 37 | destroy: Ext.ux.Ecodev.Newsletter.Remote.LinkController.destroyAction, 38 | create: Ext.ux.Ecodev.Newsletter.Remote.LinkController.createAction, 39 | }, 40 | paramOrder: { 41 | read: ['data', 'start', 'limit'], 42 | }, 43 | }); 44 | } 45 | }; 46 | 47 | /** 48 | * Public API of this singleton. 49 | */ 50 | return { 51 | initialize: initialize, 52 | }; 53 | }()); 54 | }()); 55 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Store/Recipient.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Store'); 5 | 6 | /** 7 | * A Store for the selectedRecipientList model using ExtDirect to communicate with the 8 | * server side extbase framework. 9 | */ 10 | Ext.ux.Ecodev.Newsletter.Store.Recipient = (function () { 11 | 12 | var selectedRecipientListStore = null; 13 | 14 | var initialize = function () { 15 | if (selectedRecipientListStore === null) { 16 | selectedRecipientListStore = new Ext.data.DirectStore({ 17 | storeId: 'Ecodev\\Newsletter\\Domain\\Model\\Recipient', 18 | // Here the JsonReader will be configured by metadata sent by server-side, because the columns available not known in advance 19 | reader: new Ext.data.JsonReader(), 20 | api: { 21 | read: Ext.ux.Ecodev.Newsletter.Remote.RecipientListController.listRecipientAction, 22 | }, 23 | paramOrder: { 24 | read: ['data', 'start', 'limit'], 25 | }, 26 | }); 27 | } 28 | }; 29 | 30 | /** 31 | * Public API of this singleton. 32 | */ 33 | return { 34 | initialize: initialize, 35 | }; 36 | }()); 37 | }()); 38 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Store/RecipientList.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Store'); 5 | 6 | /** 7 | * A Store for the recipientList model using ExtDirect to communicate with the 8 | * server side extbase framework. 9 | */ 10 | Ext.ux.Ecodev.Newsletter.Store.RecipientList = (function () { 11 | 12 | var recipientListStore = null; 13 | 14 | var initialize = function () { 15 | if (recipientListStore === null) { 16 | recipientListStore = new Ext.data.DirectStore({ 17 | storeId: 'Ecodev\\Newsletter\\Domain\\Model\\RecipientList', 18 | reader: new Ext.data.JsonReader({ 19 | totalProperty: 'total', 20 | successProperty: 'success', 21 | idProperty: '__identity', 22 | root: 'data', 23 | fields: [ 24 | {name: '__identity', type: 'int'}, 25 | {name: 'title', type: 'string'}, 26 | {name: 'plainOnly', type: 'boolean'}, 27 | {name: 'lang', type: 'string'}, 28 | {name: 'type', type: 'string'}, 29 | {name: 'count', type: 'integer'}, 30 | { 31 | name: 'fullName', convert: function (v, recipientList) { 32 | return String.format('{0} ({1})', recipientList.title, recipientList.count); 33 | }, 34 | }, 35 | ], 36 | }), 37 | writer: new Ext.data.JsonWriter({ 38 | encode: false, 39 | writeAllFields: false, 40 | }), 41 | api: { 42 | read: Ext.ux.Ecodev.Newsletter.Remote.RecipientListController.listAction, 43 | update: Ext.ux.Ecodev.Newsletter.Remote.RecipientListController.updateAction, 44 | destroy: Ext.ux.Ecodev.Newsletter.Remote.RecipientListController.destroyAction, 45 | create: Ext.ux.Ecodev.Newsletter.Remote.RecipientListController.createAction, 46 | }, 47 | paramOrder: { 48 | read: [], 49 | update: ['data'], 50 | create: ['data'], 51 | destroy: ['data'], 52 | }, 53 | autoLoad: true, 54 | 55 | }); 56 | } 57 | }; 58 | /** 59 | * Public API of this singleton. 60 | */ 61 | return { 62 | initialize: initialize, 63 | }; 64 | }()); 65 | }()); 66 | -------------------------------------------------------------------------------- /Resources/Public/Styles/flashmessages.css: -------------------------------------------------------------------------------- 1 | @CHARSET "UTF-8"; 2 | 3 | /** 4 | * FLASH-Messenge Boxes 5 | */ 6 | 7 | .flashmessage h3 { 8 | background-repeat: no-repeat; 9 | padding-left: 20px; 10 | } 11 | 12 | div.flashmessage { 13 | padding-left: 20px; 14 | margin-top: 10px; 15 | border: 1px solid #CCC; 16 | -webkit-border-radius: 4px; 17 | -moz-border-radius: 4px; 18 | border-radius: 4px; 19 | background-color: white; 20 | box-shadow: 2px 2px 4px 0px #AEB6C1; 21 | } 22 | 23 | div.messenge-box h3 { 24 | margin-bottom: 10px; 25 | background-color: #FFFFFF; 26 | } 27 | 28 | div.messenge-box p { 29 | margin-left: 20px; 30 | padding-bottom: 5px; 31 | } 32 | 33 | .message-ok { 34 | color: #3b7826 !important; 35 | background-image: url("../Icons/ok.svg") !important; 36 | } 37 | 38 | .message-notice { 39 | color: #777 !important; 40 | background-image: url("../Icons/information.svg") !important; 41 | } 42 | 43 | .message-information { 44 | color: #4c73a1 !important; 45 | background-image: url("../Icons/information.svg") !important; 46 | } 47 | 48 | .message-ok { 49 | color: #3b7826 !important; 50 | background-image: url("../Icons/ok.svg") !important; 51 | } 52 | 53 | .message-warning { 54 | color: #9e7d4a !important; 55 | background-image: url("../Icons/warning.svg") !important; 56 | } 57 | 58 | .message-error { 59 | color: #aa0225 !important; 60 | background-image: url("../Icons/error.svg") !important; 61 | } 62 | -------------------------------------------------------------------------------- /Tests/Build/FunctionalTests.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | ../Functional/ 9 | 10 | 11 | 12 | 13 | 14 | 15 | ../../Classes 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Tests/Build/FunctionalTestsBootstrap.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | ../Unit/ 9 | 10 | 11 | 12 | 13 | 14 | 15 | ../../Classes 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Tests/Build/UnitTestsBootstrap.php: -------------------------------------------------------------------------------- 1 | objectManager = GeneralUtility::makeInstance(ObjectManager::class); 26 | 27 | $this->importDataSet(__DIR__ . '/Fixtures/fixtures.xml'); 28 | $this->authCode = md5(302 . 'recipient2@example.com'); 29 | } 30 | 31 | /** 32 | * Assert that there is exactly 1 record in sys_log table containing 33 | * the exact text given in $details 34 | * 35 | * @param string $details 36 | */ 37 | protected function assertRecipientListCallbackWasCalled($details) 38 | { 39 | $db = $this->getDatabaseConnection(); 40 | $count = $db->exec_SELECTcountRows('*', 'sys_log', 'details = ' . $db->fullQuoteStr($details, 'sys_log')); 41 | $this->assertSame(1, $count, 'could not find exactly 1 log record containing "' . $details . '"'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/Functional/BounceHandlerTest.php: -------------------------------------------------------------------------------- 1 | dispatch(); 22 | 23 | $emailRepository = $this->objectManager->get(EmailRepository::class); 24 | $email = $emailRepository->findByUid(302); 25 | $this->assertTrue($email->isBounced()); 26 | $this->assertRecipientListCallbackWasCalled('bounced recipient2@example.com, 2, 2, 3, 4'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/Functional/Fixtures/mailer/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Newsletter Title 5 | 6 | 7 | 8 | 9 | http://www.example.com this line should never be modified 10 | this link should be injected 11 | this custom link should be injected 12 | this link should be injected, and marker substituted 13 | mailto link should still work 14 | HTML entities encoded mailto link should still work 15 | 16 | ###unknown_field_will_be_kept### 17 | ###newsletter_view_url### 18 | ###newsletter_unsubscribe_url### 19 | ###email### 20 | ###my_custom_field### 21 | 22 | http://unknown_field_will_be_kept 23 | http://newsletter_view_url 24 | http://newsletter_unsubscribe_url 25 | http://email 26 | http://my_custom_field 27 | 28 | https://unknown_field_will_be_kept 29 | https://newsletter_view_url 30 | https://newsletter_unsubscribe_url 31 | https://email 32 | https://my_custom_field 33 | 34 | boolean_false=###:IF: boolean_false ###true-ish###:ENDIF:### 35 | boolean_true=###:IF: boolean_true ###true-ish###:ENDIF:### 36 | integer_false=###:IF: integer_false ###true-ish###:ENDIF:### 37 | integer_true=###:IF: integer_true ###true-ish###:ENDIF:### 38 | string_false=###:IF: string_false ###true-ish###:ENDIF:### 39 | string_true=###:IF: string_true ###true-ish###:ENDIF:### 40 | 41 | boolean_false=###:IF: boolean_false ###true-ish###:ELSE:###false-ish###:ENDIF:### 42 | boolean_true=###:IF: boolean_true ###true-ish###:ELSE:###false-ish###:ENDIF:### 43 | integer_false=###:IF: integer_false ###true-ish###:ELSE:###false-ish###:ENDIF:### 44 | integer_true=###:IF: integer_true ###true-ish###:ELSE:###false-ish###:ENDIF:### 45 | string_false=###:IF: string_false ###true-ish###:ELSE:###false-ish###:ENDIF:### 46 | string_true=###:IF: string_true ###true-ish###:ELSE:###false-ish###:ENDIF:### 47 | 48 | UTF-8: können 한국어 ✓ € ☺ ☹ 49 | 50 | image 51 | 52 | 53 | -------------------------------------------------------------------------------- /Tests/Functional/Repository/BounceAccountRepositoryTest.php: -------------------------------------------------------------------------------- 1 | bounceAccountRepository = $this->objectManager->get(BounceAccountRepository::class); 21 | } 22 | 23 | public function testFindFirst() 24 | { 25 | $bounceAccount = $this->bounceAccountRepository->findFirst(); 26 | $this->assertNotNull($bounceAccount); 27 | $this->assertSame(666, $bounceAccount->getUid()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/Functional/Repository/EmailRepositoryTest.php: -------------------------------------------------------------------------------- 1 | emailRepository = $this->objectManager->get(EmailRepository::class); 21 | } 22 | 23 | public function testFindByAuthcode() 24 | { 25 | $email = $this->emailRepository->findByAuthcode($this->authCode); 26 | $this->assertNotNull($email); 27 | $this->assertSame(302, $email->getUid()); 28 | } 29 | 30 | public function testGetCount() 31 | { 32 | $this->assertSame(0, $this->emailRepository->getCount(10)); 33 | $this->assertSame(2, $this->emailRepository->getCount(30)); 34 | } 35 | 36 | public function testFindAllByNewsletter() 37 | { 38 | $this->assertCount(0, $this->emailRepository->findAllByNewsletter(10, 0, 999)); 39 | 40 | $emails = $this->emailRepository->findAllByNewsletter(30, 0, 999); 41 | $this->assertCount(2, $emails); 42 | $this->assertSame(301, $emails[0]->getUid()); 43 | $this->assertSame(302, $emails[1]->getUid()); 44 | 45 | $emails = $this->emailRepository->findAllByNewsletter(30, 1, 999); 46 | $this->assertCount(1, $emails); 47 | $this->assertSame(302, $emails[0]->getUid()); 48 | 49 | $emails = $this->emailRepository->findAllByNewsletter(30, 2, 999); 50 | $this->assertCount(0, $emails); 51 | 52 | $emails = $this->emailRepository->findAllByNewsletter(30, 0, 1); 53 | $this->assertCount(1, $emails); 54 | $this->assertSame(301, $emails[0]->getUid()); 55 | 56 | $emails = $this->emailRepository->findAllByNewsletter(30, 1, 1); 57 | $this->assertCount(1, $emails); 58 | $this->assertSame(302, $emails[0]->getUid()); 59 | 60 | $emails = $this->emailRepository->findAllByNewsletter(30, 2, 1); 61 | $this->assertCount(0, $emails); 62 | } 63 | 64 | public function testRegisterOpen() 65 | { 66 | $this->emailRepository->registerOpen($this->authCode); 67 | 68 | $email = $this->emailRepository->findByUid(302); 69 | $this->assertTrue($email->isOpened(), 'email should be marked as opened'); 70 | $this->assertRecipientListCallbackWasCalled('opened recipient2@example.com'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/Functional/Repository/RecipientListRepositoryTest.php: -------------------------------------------------------------------------------- 1 | recipientListRepository = $this->objectManager->get(RecipientListRepository::class); 21 | } 22 | 23 | public function testFindByUidInitialized() 24 | { 25 | $recipientList = $this->recipientListRepository->findByUidInitialized(1000); 26 | $this->assertNotNull($recipientList); 27 | $this->assertSame(2, $recipientList->getCount(), 'should not have to call init() to get count'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/Functional/ToolsTest.php: -------------------------------------------------------------------------------- 1 | importDataSet(ORIGINAL_ROOT . 'typo3/sysext/core/Tests/Functional/Fixtures/tt_content.xml'); 19 | 20 | $db = $this->getDatabaseConnection(); 21 | $count = $db->exec_SELECTcountRows('*', 'tx_newsletter_domain_model_newsletter', 'begin_time != 0 AND end_time != 0'); 22 | $this->assertSame(0, $count); 23 | 24 | Tools::createAllSpool(); 25 | 26 | $count = $db->exec_SELECTcountRows('*', 'tx_newsletter_domain_model_newsletter', 'begin_time != 0 AND end_time != 0'); 27 | $this->assertSame(1, $count, 'newsletter should be marked as spooled'); 28 | 29 | $count = $db->exec_SELECTcountRows('*', 'tx_newsletter_domain_model_email', 'newsletter = 20 AND begin_time = 0'); 30 | $this->assertSame(2, $count, 'two emails must have been created but not sent yet'); 31 | 32 | $lastInsertedEmail = $db->exec_SELECTgetSingleRow('*', 'tx_newsletter_domain_model_email', 'newsletter = 20 AND begin_time = 0'); 33 | $this->assertNotSame(md5('0' . $lastInsertedEmail['recipient_address']), $lastInsertedEmail['auth_code'], 'the UID used in authCode must never be 0'); 34 | $this->assertSame(md5($lastInsertedEmail['uid'] . $lastInsertedEmail['recipient_address']), $lastInsertedEmail['auth_code'], 'the UID used in authCode should be the real value'); 35 | 36 | // Prepare a mock to always validate content 37 | /** @var Validator|\PHPUnit_Framework_MockObject_MockObject $mockValidator */ 38 | $mockValidator = $this->getMock(Validator::class, ['validate'], [], '', false); 39 | $mockValidator->method('validate')->will($this->returnValue( 40 | [ 41 | 'content' => 'some very interesting content link', 42 | 'errors' => [], 43 | 'warnings' => [], 44 | 'infos' => [], 45 | ] 46 | )); 47 | 48 | // Force email to NOT be sent 49 | global $TYPO3_CONF_VARS; 50 | $TYPO3_CONF_VARS['MAIL']['transport'] = 'Swift_NullTransport'; 51 | 52 | /** @var NewsletterRepository $newsletterRepository */ 53 | $newsletterRepository = $this->objectManager->get(NewsletterRepository::class); 54 | $newsletter = $newsletterRepository->findByUid(20); 55 | $newsletter->setValidator($mockValidator); 56 | Tools::runSpool($newsletter); 57 | 58 | $count = $db->exec_SELECTcountRows('*', 'tx_newsletter_domain_model_email', 'newsletter = 20 AND begin_time != 0 AND end_time != 0 AND recipient_data != ""'); 59 | $this->assertSame(2, $count, 'should have sent two emails'); 60 | $count = $db->exec_SELECTcountRows('*', 'tx_newsletter_domain_model_link', 'newsletter = 20'); 61 | $this->assertSame(1, $count, 'should have on1 new link'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/Functional/Utility/UriBuilderTest.php: -------------------------------------------------------------------------------- 1 | 1, 'b' => 'baz']); 16 | $expected = '/?tx_newsletter_p%5Ba%5D=1&tx_newsletter_p%5Bb%5D=baz&tx_newsletter_p%5Baction%5D=bar&tx_newsletter_p%5Bcontroller%5D=foo&type=1342671779'; 17 | $this->assertSame($expected, $actual); 18 | } 19 | 20 | public function testBuildFrontendUri() 21 | { 22 | // Create an internal builder mock 23 | $mockBuilder = $this->getMock(\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder::class, ['buildFrontendUri'], [], '', false); 24 | $mockBuilder->method('buildFrontendUri')->will($this->onConsecutiveCalls('url1?a=1', 'url2?a=1')); 25 | 26 | // Inject the mock 27 | $reflectionClass = new ReflectionClass(UriBuilder::class); 28 | $property = $reflectionClass->getProperty('uriBuilder'); 29 | $property->setAccessible(true); 30 | $property->setValue([1 => $mockBuilder]); 31 | 32 | $actual1 = UriBuilder::buildFrontendUri(1, 'foo', 'bar', ['a' => 1, 'l' => 'original']); 33 | $expectedArguments = [ 34 | 'tx_newsletter_p' => [ 35 | 'a' => 1, 36 | 'action' => 'bar', 37 | 'controller' => 'foo', 38 | ], 39 | ]; 40 | $this->assertSame($expectedArguments, $mockBuilder->getArguments(), 'the internal builder should have been given namespaced arguments without the special l parameter'); 41 | 42 | $actual2 = UriBuilder::buildFrontendUri(1, 'foo', 'bar', ['baz' => 1]); 43 | $actual1bis = UriBuilder::buildFrontendUri(1, 'foo', 'bar', ['a' => 1, 'l' => 'bis']); 44 | 45 | $this->assertSame('url1?a=1&tx_newsletter_p%5Bl%5D=original', $actual1, 'should be able to build URL with special l parameter'); 46 | $this->assertSame('url2?a=1', $actual2, 'should be able to build URL without special l parameter'); 47 | $this->assertSame('url1?a=1&tx_newsletter_p%5Bl%5D=bis', $actual1bis, 'should be able to hit the cache to retrieve the first URL and complete new value of l parameter'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/Unit/AbstractUnitTestCase.php: -------------------------------------------------------------------------------- 1 | getLocalConfigurationFileLocation(); 13 | 14 | if (is_readable($path)) { 15 | $allConfig = $manager->getLocalConfiguration(); 16 | $config = $allConfig['EXT']['extConf']['newsletter']; 17 | } 18 | 19 | if (!isset($config)) { 20 | $config = serialize([ 21 | 'path_to_lynx' => '/usr/bin/lynx', 22 | 'replyto_name' => 'John Connor', 23 | 'replyto_email' => 'john.connor@example.com', 24 | ]); 25 | } 26 | 27 | $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['newsletter'] = $config; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/Unit/Controller/BounceAccountControllerTest.php: -------------------------------------------------------------------------------- 1 | subject = $this->getMock(BounceAccountController::class, ['redirect', 'forward', 'addFlashMessage', 'translate', 'flushFlashMessages'], [], '', false); 23 | } 24 | 25 | protected function tearDown() 26 | { 27 | unset($this->subject); 28 | } 29 | 30 | /** 31 | * @test 32 | */ 33 | public function listActionFetchesAllBounceAccountsFromRepositoryAndAssignsThemToView() 34 | { 35 | $allBounceAccounts = $this->getMock(ObjectStorage::class, [], [], '', false); 36 | 37 | $bounceAccountRepository = $this->getMock(BounceAccountRepository::class, ['findAll'], [], '', false); 38 | $bounceAccountRepository->expects($this->once())->method('findAll')->will($this->returnValue($allBounceAccounts)); 39 | $this->inject($this->subject, 'bounceAccountRepository', $bounceAccountRepository); 40 | 41 | $view = $this->getMock(ExtDirectView::class, ['assign']); 42 | $view->expects($this->at(0))->method('assign')->with('total', count($allBounceAccounts)); 43 | $view->expects($this->at(1))->method('assign')->with('data', $allBounceAccounts); 44 | $view->expects($this->at(2))->method('assign')->with('success', true); 45 | $this->inject($this->subject, 'view', $view); 46 | 47 | $this->subject->listAction(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/LinkTest.php: -------------------------------------------------------------------------------- 1 | subject = new Link(); 21 | } 22 | 23 | protected function tearDown() 24 | { 25 | unset($this->subject); 26 | } 27 | 28 | /** 29 | * @test 30 | */ 31 | public function getUrlReturnsInitialValueForString() 32 | { 33 | $this->assertSame('', $this->subject->getUrl()); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function setUrlForStringSetsUrl() 40 | { 41 | $this->subject->setUrl('Conceived at T3CON10'); 42 | $this->assertAttributeSame('Conceived at T3CON10', 'url', $this->subject); 43 | } 44 | 45 | /** 46 | * @test 47 | */ 48 | public function setNewsletterForNewsletterSetsNewsletter() 49 | { 50 | $newsletterFixture = new Newsletter(); 51 | $this->subject->setNewsletter($newsletterFixture); 52 | 53 | $this->assertAttributeSame( 54 | $newsletterFixture, 'newsletter', $this->subject 55 | ); 56 | } 57 | 58 | /** 59 | * @test 60 | */ 61 | public function getOpenedCountReturnsInitialValueForInteger() 62 | { 63 | $this->assertSame( 64 | 0, $this->subject->getOpenedCount() 65 | ); 66 | } 67 | 68 | /** 69 | * @test 70 | */ 71 | public function setOpenedCountForIntegerSetsOpenedCount() 72 | { 73 | $this->subject->setOpenedCount(12); 74 | 75 | $this->assertAttributeSame( 76 | 12, 'openedCount', $this->subject 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/PlainConverter/BuiltinTest.php: -------------------------------------------------------------------------------- 1 | subject = new Builtin(); 20 | } 21 | 22 | protected function tearDown() 23 | { 24 | unset($this->subject); 25 | } 26 | 27 | /** 28 | * @test 29 | */ 30 | public function getUrlReturnsInitialValueForString() 31 | { 32 | $html = file_get_contents(__DIR__ . '/input.html'); 33 | $expected = file_get_contents(__DIR__ . '/builtin.txt'); 34 | $actual = $this->subject->getPlainText($html, 'http://my-domain.com'); 35 | $this->assertSame($expected, $actual); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/PlainConverter/LynxTest.php: -------------------------------------------------------------------------------- 1 | subject = new Lynx(); 21 | } 22 | 23 | protected function tearDown() 24 | { 25 | unset($this->subject); 26 | } 27 | 28 | private function canRunLynx() 29 | { 30 | $this->loadConfiguration(); 31 | 32 | $cmd = escapeshellcmd(Tools::confParam('path_to_lynx')) . ' --help'; 33 | exec($cmd, $output, $statusCode); 34 | 35 | return $statusCode == 0; 36 | } 37 | 38 | /** 39 | * @test 40 | */ 41 | public function getUrlReturnsInitialValueForString() 42 | { 43 | if (!$this->canRunLynx()) { 44 | $this->markTestSkipped('The command "' . Tools::confParam('path_to_lynx') . '" is not available.'); 45 | } 46 | 47 | $html = file_get_contents(__DIR__ . '/input.html'); 48 | $expected = file_get_contents(__DIR__ . '/lynx.txt'); 49 | $actual = $this->subject->getPlainText($html, 'http://my-domain.com'); 50 | $this->assertSame($expected, $actual); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/PlainConverter/builtin.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | FIRST TITLE 4 | 5 | SECOND TITLE 6 | 7 | Muffin cake tootsie roll chocolate cake cookie. Gummi bears sugar plum 8 | powder icing jujubes tart danish caramels. Cotton candy jujubes 9 | pudding chocolate ice cream sesame snaps. Souffle cookie marzipan 10 | chocolate cake croissant lemon drops. 11 | 12 | Icing souffle sugar plum ice cream marzipan. Jelly tart powder 13 | fruitcake oat cake halvah chocolate cake halvah icing. Gummies candy 14 | canes liquorice. Dragee pastry sesame snaps cake cake gingerbread. 15 | 16 | * item 1 17 | * item 2 18 | * item 3 19 | 20 | first link [1] second link [2] third identical link [1] relative link 21 | [3] 22 | 23 | 24 | Links: 25 | ------ 26 | [1] http://example.com 27 | [2] http://example.com/2 28 | [3] http://my-domain.com/my-page 29 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/PlainConverter/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 |

First title

11 |

Second title

12 | 13 |

Muffin cake tootsie roll chocolate cake cookie. Gummi bears sugar 14 | plum powder icing jujubes tart danish caramels. Cotton candy jujubes 15 | pudding chocolate ice cream sesame snaps. Souffle cookie marzipan 16 | chocolate cake croissant lemon drops.

17 |

Icing souffle sugar plum ice cream marzipan. Jelly tart powder 18 | fruitcake oat cake halvah chocolate cake halvah icing. Gummies 19 | candy canes liquorice. Dragee pastry sesame snaps cake cake 20 | gingerbread.

21 | 22 |
    23 |
  • item 1
  • 24 |
  • item 2
  • 25 |
  • item 3
  • 26 |
27 | 28 |

29 | first link 30 | second link 31 | third identical link 32 | relative link 33 |

34 | 35 | 36 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/PlainConverter/lynx.txt: -------------------------------------------------------------------------------- 1 | First title 2 | 3 | Second title 4 | 5 | Muffin cake tootsie roll chocolate cake cookie. Gummi bears sugar plum 6 | powder icing jujubes tart danish caramels. Cotton candy jujubes pudding 7 | chocolate ice cream sesame snaps. Souffle cookie marzipan chocolate 8 | cake croissant lemon drops. 9 | 10 | Icing souffle sugar plum ice cream marzipan. Jelly tart powder 11 | fruitcake oat cake halvah chocolate cake halvah icing. Gummies candy 12 | canes liquorice. Dragee pastry sesame snaps cake cake gingerbread. 13 | * item 1 14 | * item 2 15 | * item 3 16 | 17 | [1]first link [2]second link [3]third identical link [4]relative link 18 | 19 | References 20 | 21 | 1. http://example.com/ 22 | 2. http://example.com/2 23 | 3. http://example.com/ 24 | 4. http://my-domain.com/my-page -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/RecipientList/AbstractRecipientList.php: -------------------------------------------------------------------------------- 1 | subject); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function getTitleReturnsInitialValueForString() 24 | { 25 | $this->assertSame('', $this->subject->getTitle()); 26 | } 27 | 28 | /** 29 | * @test 30 | */ 31 | public function setTitleForStringSetsTitle() 32 | { 33 | $this->subject->setTitle('Conceived at T3CON10'); 34 | $this->assertAttributeSame('Conceived at T3CON10', 'title', $this->subject); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function getPlainOnlyReturnsInitialValueForBoolean() 41 | { 42 | $this->assertFalse( 43 | $this->subject->getPlainOnly() 44 | ); 45 | $this->assertFalse( 46 | $this->subject->isPlainOnly() 47 | ); 48 | } 49 | 50 | /** 51 | * @test 52 | */ 53 | public function setPlainOnlyForBooleanSetsPlainOnly() 54 | { 55 | $this->subject->setPlainOnly(true); 56 | 57 | $this->assertAttributeSame( 58 | true, 'plainOnly', $this->subject 59 | ); 60 | } 61 | 62 | /** 63 | * @test 64 | */ 65 | public function getLangReturnsInitialValueForString() 66 | { 67 | $this->assertSame(0, $this->subject->getLang()); 68 | } 69 | 70 | /** 71 | * @test 72 | */ 73 | public function setLangForStringSetsLang() 74 | { 75 | $this->subject->setLang(123); 76 | $this->assertAttributeSame(123, 'lang', $this->subject); 77 | } 78 | 79 | /** 80 | * @test 81 | */ 82 | public function getTypeReturnsInitialValueForString() 83 | { 84 | $this->assertSame('', $this->subject->getType()); 85 | } 86 | 87 | /** 88 | * @test 89 | */ 90 | public function setTypeForStringSetsType() 91 | { 92 | $this->subject->setType('Conceived at T3CON10'); 93 | $this->assertAttributeSame('Conceived at T3CON10', 'type', $this->subject); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/RecipientList/CsvFileTest.php: -------------------------------------------------------------------------------- 1 | subject = new CsvFile(); 15 | } 16 | 17 | /** 18 | * @test 19 | */ 20 | public function getCsvSeparatorReturnsInitialValueForString() 21 | { 22 | $this->assertSame(',', $this->subject->getCsvSeparator()); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function setCsvSeparatorForStringSetsCsvSeparator() 29 | { 30 | $this->subject->setCsvSeparator('Conceived at T3CON10'); 31 | $this->assertAttributeSame('Conceived at T3CON10', 'csvSeparator', $this->subject); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function getCsvFieldsReturnsInitialValueForString() 38 | { 39 | $this->assertSame('', $this->subject->getCsvFields()); 40 | } 41 | 42 | /** 43 | * @test 44 | */ 45 | public function setCsvFieldsForStringSetsCsvFields() 46 | { 47 | $this->subject->setCsvFields('Conceived at T3CON10'); 48 | $this->assertAttributeSame('Conceived at T3CON10', 'csvFields', $this->subject); 49 | } 50 | 51 | /** 52 | * @test 53 | */ 54 | public function getCsvFilenameReturnsInitialValueForString() 55 | { 56 | $this->assertSame('', $this->subject->getCsvFilename()); 57 | } 58 | 59 | /** 60 | * @test 61 | */ 62 | public function setCsvFilenameForStringSetsCsvFilename() 63 | { 64 | $this->subject->setCsvFilename('Conceived at T3CON10'); 65 | $this->assertAttributeSame('Conceived at T3CON10', 'csvFilename', $this->subject); 66 | } 67 | 68 | protected function prepareDataForEnumeration() 69 | { 70 | $this->subject = $this->getMock(CsvFile::class, ['getPathname'], [], '', false); 71 | $this->subject->expects($this->once())->method('getPathname')->will($this->returnValue(__DIR__)); 72 | $this->subject->setCsvFilename('data.csv'); 73 | } 74 | 75 | /** 76 | * @test 77 | */ 78 | public function canEnumerateRecipients() 79 | { 80 | $this->prepareDataForEnumeration(); 81 | $this->subject->setCsvFields('email,name,some_flags'); 82 | $this->subject->init(); 83 | $this->assertSame(2, $this->subject->getCount()); 84 | 85 | $recipient1 = [ 86 | 'email' => 'john@example.com', 87 | 'name' => 'John', 88 | 'some_flags' => '1', 89 | 'plain_only' => false, 90 | ]; 91 | 92 | $recipient2 = [ 93 | 'email' => 'bob@example.com', 94 | 'name' => 'Roger', 95 | 'some_flags' => '0', 96 | 'plain_only' => false, 97 | ]; 98 | 99 | $this->assertSame($recipient1, $this->subject->getRecipient()); 100 | $this->assertSame($recipient2, $this->subject->getRecipient()); 101 | $this->assertFalse($this->subject->getRecipient()); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/RecipientList/CsvListTest.php: -------------------------------------------------------------------------------- 1 | subject = new CsvList(); 15 | } 16 | 17 | /** 18 | * @test 19 | */ 20 | public function getCsvSeparatorReturnsInitialValueForString() 21 | { 22 | $this->assertSame(',', $this->subject->getCsvSeparator()); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function getCsvValuesReturnsInitialValueForString() 29 | { 30 | $this->assertSame('', $this->subject->getCsvValues()); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function setCsvValuesForStringSetsCsvValues() 37 | { 38 | $this->subject->setCsvValues('Conceived at T3CON10'); 39 | $this->assertAttributeSame('Conceived at T3CON10', 'csvValues', $this->subject); 40 | } 41 | 42 | protected function prepareDataForEnumeration() 43 | { 44 | $values = file_get_contents(__DIR__ . '/data.csv'); 45 | 46 | $this->subject->setCsvValues($values); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/RecipientList/CsvUrlTest.php: -------------------------------------------------------------------------------- 1 | subject = new CsvUrl(); 15 | } 16 | 17 | /** 18 | * @test 19 | */ 20 | public function getCsvUrlReturnsInitialValueForString() 21 | { 22 | $this->assertSame('', $this->subject->getCsvUrl()); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function setCsvUrlForStringSetsCsvUrl() 29 | { 30 | $this->subject->setCsvUrl('Conceived at T3CON10'); 31 | $this->assertAttributeSame('Conceived at T3CON10', 'csvUrl', $this->subject); 32 | } 33 | 34 | protected function prepareDataForEnumeration() 35 | { 36 | $this->subject->setCsvUrl(__DIR__ . '/data.csv'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/RecipientList/SqlTest.php: -------------------------------------------------------------------------------- 1 | subject = new Sql(); 15 | } 16 | 17 | /** 18 | * @test 19 | */ 20 | public function getSqlStatementReturnsInitialValueForString() 21 | { 22 | $this->assertSame('', $this->subject->getSqlStatement()); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function setSqlStatementForStringSetsSqlStatement() 29 | { 30 | $this->subject->setSqlStatement('Conceived at T3CON10'); 31 | $this->assertAttributeSame('Conceived at T3CON10', 'sqlStatement', $this->subject); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function getSqlRegisterBounceReturnsInitialValueForString() 38 | { 39 | $this->assertSame('', $this->subject->getSqlRegisterBounce()); 40 | } 41 | 42 | /** 43 | * @test 44 | */ 45 | public function setSqlRegisterBounceForStringSetsSqlRegisterBounce() 46 | { 47 | $this->subject->setSqlRegisterBounce('Conceived at T3CON10'); 48 | $this->assertAttributeSame('Conceived at T3CON10', 'sqlRegisterBounce', $this->subject); 49 | } 50 | 51 | /** 52 | * @test 53 | */ 54 | public function getSqlRegisterClickReturnsInitialValueForString() 55 | { 56 | $this->assertSame('', $this->subject->getSqlRegisterClick()); 57 | } 58 | 59 | /** 60 | * @test 61 | */ 62 | public function setSqlRegisterClickForStringSetsSqlRegisterClick() 63 | { 64 | $this->subject->setSqlRegisterClick('Conceived at T3CON10'); 65 | $this->assertAttributeSame('Conceived at T3CON10', 'sqlRegisterClick', $this->subject); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/Unit/Domain/Model/RecipientList/data.csv: -------------------------------------------------------------------------------- 1 | john@example.com,John,1 2 | bob@example.com,Roger,0 3 | -------------------------------------------------------------------------------- /Tests/Unit/Fixtures/bounce/2-87c4e9b09085befbb7f20faa7482213a-Undelivered Mail Returned to Sender.eml: -------------------------------------------------------------------------------- 1 | Delivered-To: bounce@example.com 2 | Received: by 10.10.10.10 with SMTP id a59csp784285qge; 3 | Thu, 8 Jan 2015 00:07:47 -0800 (PST) 4 | From: MAILER-DAEMON@example.com (Mail Delivery System) 5 | Subject: Undelivered Mail Returned to Sender 6 | To: recipient2@example.com 7 | Message-ID: <87c4e9b09085befbb7f20faa7482213a@example.com> 8 | 9 | This is the mail system at host mail.example.com. 10 | 11 | I'm sorry to have to inform you that your message could not 12 | be delivered to one or more recipients. It's attached below. 13 | 14 | For further assistance, please send mail to postmaster. 15 | 16 | If you do so, please include this problem report. You can 17 | delete your own text from the attached returned message. 18 | 19 | The mail system 20 | 21 | 22 | -------------------------------------------------------------------------------- /Tests/Unit/ToolsTest.php: -------------------------------------------------------------------------------- 1 | assertNotSame('my value', $encrypted, 'must be encrypted'); 17 | $decrypted = Tools::decrypt($encrypted); 18 | $this->assertSame('my value', $decrypted, 'must be original value'); 19 | } 20 | 21 | public function testUserAgent() 22 | { 23 | define('TYPO3_user_agent', 'User-Agent: TYPO3'); 24 | $userAgent = Tools::getUserAgent(); 25 | $this->assertSame(1, preg_match('~^User-Agent: TYPO3 Newsletter \(https://github.com/Ecodev/newsletter\)$~', $userAgent)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/Unit/Utility/EmailParserTest.php: -------------------------------------------------------------------------------- 1 | parse($emailSource); 42 | $this->assertSame($expectedBounce, $parser->getBounceLevel(), 'Bounce level should be ' . var_export($expectedBounce, true) . ' for ' . $message); 43 | $this->assertSame($expectedAuthCode, $parser->getAuthCode(), 'AuthCode should be ' . var_export($expectedAuthCode, true) . ' for ' . $message); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/Unit/Utility/UriTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Uri::isAbsolute($scheme . ':foo/bar')); 30 | $this->assertTrue(Uri::isAbsolute($scheme . '://foo/bar')); 31 | $this->assertFalse(Uri::isAbsolute('foo/bar/' . $scheme)); 32 | } 33 | 34 | public function testFragment() 35 | { 36 | $this->assertTrue(Uri::isAbsolute('http:foo/bar#abc')); 37 | $this->assertTrue(Uri::isAbsolute('#abc'), 'fragment URL should be considered absolute URI because they msut not be modified at all'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /class.ext_update.php: -------------------------------------------------------------------------------- 1 | update = new Update(); 18 | } 19 | 20 | /** 21 | * Do update and return result as HTML 22 | * 23 | * @return string HTML content 24 | */ 25 | public function main() 26 | { 27 | return $this->update->update(); 28 | } 29 | 30 | /** 31 | * Return whether update is required 32 | * 33 | * @return bool 34 | */ 35 | public function access() 36 | { 37 | $queries = $this->update->getQueries(); 38 | 39 | return !empty($queries); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cli/.htaccess: -------------------------------------------------------------------------------- 1 | deny from all 2 | -------------------------------------------------------------------------------- /cli/bounce.php: -------------------------------------------------------------------------------- 1 | dispatch(); 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecodev/newsletter", 3 | "type": "typo3-cms-extension", 4 | "description": "TYPO3 extension to send any pages as Newsletter and provide statistics on opened emails and clicked links.", 5 | "homepage": "https://github.com/ecodev/newsletter", 6 | "license": "GPL-2.0+", 7 | "keywords": [ 8 | "TYPO3 CMS", 9 | "TYPO3", 10 | "newsletter", 11 | "email", 12 | "statistics" 13 | ], 14 | "require": { 15 | "php": ">=5.6.0", 16 | "ext-dom": "*", 17 | "ext-openssl": "*", 18 | "typo3/cms-backend": "^7.6 || ^8.7", 19 | "typo3/cms-core": "^7.6 || ^8.7", 20 | "typo3/cms-extbase": "^7.6 || ^8.7", 21 | "typo3/cms-fluid": "^7.6 || ^8.7", 22 | "typo3/cms-scheduler": "^7.6 || ^8.7" 23 | }, 24 | "require-dev": { 25 | "friendsofphp/php-cs-fixer": "^2.1" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Ecodev\\Newsletter\\": "Classes/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Ecodev\\Newsletter\\Tests\\": "Tests/" 35 | } 36 | }, 37 | "extra": { 38 | "typo3/cms": { 39 | "cms-package-dir": "{$vendor-dir}/typo3/cms", 40 | "web-dir": "web" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ext_conf_template.txt: -------------------------------------------------------------------------------- 1 | # cat=basic; type=string; label=Default sender name: Specify default value for sender name. Can be overridden for each newsletter. If "user", the be_user owning the newsletter will be used. If blank, will default to the site name from $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']. Otherwise can be any string. 2 | sender_name = user 3 | 4 | # cat=basic; type=string; label=Default sender email: Specify default value for sender email. Can be overridden for each newsletter. If "user", the be_user owning the newsletter will be used. If blank, will default to generic settings for the site or the system. Otherwise must be a valid email address. 5 | sender_email = user 6 | 7 | # cat=basic; type=string; label=Notification email: Specify a valid address email to receive notification when recipients unsubscribe. If "user", the be_user owning the newsletter will be used. If blank, no notification will be sent. Otherwise must be a valid email address. 8 | notification_email = user 9 | 10 | # cat=basic; type=string; label=Domain: Base URL (scheme + domain + path) from which to fetch content and encode links with. Leave blank to use domain-records from the page tree. 11 | fetch_path = 12 | 13 | # cat=basic; type=string; label=Append parameters: String to append to URL's when fetching content. For example use &type= allows you to implement your newsletter with a special template. Or disable cache with &no_cache=1. 14 | append_url = 15 | 16 | # cat=basic; type=string; label=Path to Lynx CLI browser: This program is not required, but can be used by Newsletter to produce an acceptable plaintext conversion. 17 | path_to_lynx = /usr/bin/lynx 18 | 19 | # cat=basic; type=string; label=Path to fetchmail program: This a standard unix mail-retrieval program. It is used to collect bounced emails with. 20 | path_to_fetchmail = /usr/bin/fetchmail 21 | 22 | # cat=basic; type=boolean; label=Keep bounced emails on server: Checking this option will leave bounced emails on the server. Default behaviour is to *delete* bounce messages, once they have been processed. 23 | keep_messages = 0 24 | 25 | # cat=basic; type=boolean; label=Attach images: Leave this checked to enable attached, inline images. This will normally improve you viewers experience, but reduces the performance of the mailer, since it has to deliver much more (binary) data. Uncheck it to instead link to the images online. 26 | attach_images = 1 27 | 28 | # cat=basic; type=integer; label=Number of mailer per round: This can be used to limit the rate on which Newsletter will send out mails. You must set this if you wish to use the "Invoke mailer" button. If you specify 0 as value, it will send all mails in one go. 29 | mails_per_round = 100 30 | 31 | # cat=basic; type=string; label=Redirect page (pid|url) on unsubscribe: If this value is set, on unsubscribe the user will be redirected to the corresponding page. 32 | unsubscribe_redirect = 33 | -------------------------------------------------------------------------------- /ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'Newsletter', 5 | 'description' => 'Send any pages as Newsletter and provide statistics on opened emails and clicked links.', 6 | 'category' => 'module', 7 | 'version' => '4.0.0', 8 | 'state' => 'stable', 9 | 'uploadfolder' => 1, 10 | 'author' => 'Ecodev', 11 | 'author_email' => 'contact@ecodev.ch', 12 | 'author_company' => 'Ecodev', 13 | 'constraints' => [ 14 | 'depends' => [ 15 | 'php' => '5.6.0-0.0.0', 16 | 'typo3' => '7.6.0-8.99.99', 17 | 'scheduler' => '7.6.0-8.99.99', 18 | ], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /ext_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ext_localconf.php: -------------------------------------------------------------------------------- 1 | '); 9 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptConstants(''); 10 | 11 | // Register keys for CLI 12 | $TYPO3_CONF_VARS['SC_OPTIONS']['GLOBAL']['cliKeys']['newsletter_bounce'] = ['EXT:newsletter/cli/bounce.php', '_CLI_scheduler']; 13 | 14 | // Configure FE plugin element 15 | \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( 16 | 'Ecodev.' . $_EXTKEY, 'p', [// list of controller 17 | 'Email' => 'show, opened', 18 | 'Link' => 'clicked', 19 | 'RecipientList' => 'unsubscribe, export', 20 | ], [// non-cacheable controller 21 | 'Email' => 'show, opened, unsubscribe', 22 | 'Link' => 'clicked', 23 | 'RecipientList' => 'export', 24 | ] 25 | ); 26 | 27 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\Ecodev\Newsletter\Task\SendEmails::class] = [ 28 | 'extension' => $_EXTKEY, 29 | 'title' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang.xlf:task_send_emails_title', 30 | 'description' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang.xlf:task_send_emails_description', 31 | ]; 32 | 33 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\Ecodev\Newsletter\Task\FetchBounces::class] = [ 34 | 'extension' => $_EXTKEY, 35 | 'title' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang.xlf:task_fetch_bounces_title', 36 | 'description' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang.xlf:task_fetch_bounces_description', 37 | ]; 38 | 39 | // Configure TCA custom eval and hooks to manage on-the-fly (de)encryption from database 40 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][\Ecodev\Newsletter\Tca\BounceAccountTca::class] = 'EXT:' . $_EXTKEY . '/Classes/Tca/BounceAccountTca.php'; 41 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaDatabaseRecord'][\Ecodev\Newsletter\Tca\BounceAccountDataProvider::class] = [ 42 | 'depends' => [ 43 | \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseEditRow::class, 44 | ], 45 | ]; 46 | 47 | // Make a call to update 48 | if (TYPO3_MODE === 'BE') { 49 | $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class); 50 | $dispatcher->connect(\TYPO3\CMS\Extensionmanager\Utility\InstallUtility::class, 'afterExtensionInstall', \Ecodev\Newsletter\Update\Update::class, 'afterExtensionInstall'); 51 | } 52 | -------------------------------------------------------------------------------- /ext_tables.php: -------------------------------------------------------------------------------- 1 | 'index', 16 | 'Newsletter' => 'list, listPlanned, create, statistics', 17 | 'Email' => 'list', 18 | 'Link' => 'list', 19 | 'BounceAccount' => 'list', 20 | 'RecipientList' => 'list, listRecipient', 21 | ], 22 | [ 23 | 'access' => 'user,group', 24 | 'icon' => 'EXT:newsletter/Resources/Public/Icons/tx_newsletter.svg', 25 | 'labels' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang_module.xlf', 26 | ] 27 | ); 28 | } 29 | 30 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('tx_newsletter_domain_model_newsletter', 'EXT:newsletter/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_newsletter.xlf'); 31 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_newsletter_domain_model_newsletter'); 32 | 33 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('tx_newsletter_domain_model_bounceaccount', 'EXT:newsletter/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf'); 34 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_newsletter_domain_model_bounceaccount'); 35 | 36 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('tx_newsletter_domain_model_recipientlist', 'EXT:newsletter/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_recipientlist.xlf'); 37 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_newsletter_domain_model_recipientlist'); 38 | 39 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('tx_newsletter_domain_model_email', 'EXT:newsletter/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_email.xlf'); 40 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_newsletter_domain_model_email'); 41 | 42 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('tx_newsletter_domain_model_link', 'EXT:newsletter/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_link.xlf'); 43 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_newsletter_domain_model_link'); 44 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var shell = require('gulp-shell'); 3 | 4 | var paths = { 5 | js: [ 6 | 'Resources/Public/JavaScript/**/*.js', 7 | '!Resources/Public/JavaScript/NVD3/*', 8 | '!Resources/Public/JavaScript/Override/*', 9 | ], 10 | php: [ 11 | '**/*.php', 12 | '!3dparty/*', 13 | '!vendor/*', 14 | ], 15 | }; 16 | 17 | gulp.task('default', ['composer', 'lint-js', 'lint-php'], function() { 18 | // place code for your default task here 19 | }); 20 | 21 | gulp.task('lint-js', function() { 22 | var jshint = require('gulp-jshint'); 23 | var jscs = require('gulp-jscs'); 24 | 25 | return gulp.src(paths.js) 26 | .pipe(jscs({configPath: '.jscs.json'})) 27 | .pipe(jshint()) 28 | .pipe(jshint.reporter()); 29 | }); 30 | 31 | gulp.task('lint-php', ['composer'], shell.task([ 32 | './vendor/bin/php-cs-fixer fix --diff --verbose --dry-run' 33 | ])); 34 | 35 | gulp.task('composer', function() { 36 | var composer = require('gulp-composer'); 37 | return composer('install', {}); 38 | }); 39 | 40 | gulp.task('watch', function() { 41 | gulp.watch(paths.js, ['lint-js']); 42 | gulp.watch(paths.php, ['lint-php']); 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Newsletter", 3 | "private": true, 4 | "dependencies": { 5 | "gulp": "^3.9.1", 6 | "gulp-composer": "^0.4.5", 7 | "gulp-jscs": "^4.1.0", 8 | "gulp-jshint": "^2.1.0", 9 | "gulp-shell": "^0.6.5", 10 | "jshint": "^2.9.5" 11 | } 12 | } 13 | --------------------------------------------------------------------------------