diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 423a62b159b..c722f0a597d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -102,6 +102,7 @@ include: - local: .gitlab/ci/qa.gitlab-ci.yml - local: .gitlab/ci/reports.gitlab-ci.yml - local: .gitlab/ci/rails.gitlab-ci.yml + - local: .gitlab/ci/vendored-gems.gitlab-ci.yml - local: .gitlab/ci/review.gitlab-ci.yml - local: .gitlab/ci/rules.gitlab-ci.yml - local: .gitlab/ci/setup.gitlab-ci.yml diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 8062f3d59cb..5dc156fbcaa 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -920,6 +920,16 @@ - <<: *if-merge-request changes: [".gitlab/ci/rails.gitlab-ci.yml"] +####################### +# Vendored gems rules # +####################### + +.vendor:rules:mail-smtp_pool: + rules: + - <<: *if-merge-request + changes: ["vendor/gems/mail-smtp_pool/**/*"] + - <<: *if-merge-request-title-run-all-rspec + ################## # Releases rules # ################## diff --git a/.gitlab/ci/vendored-gems.gitlab-ci.yml b/.gitlab/ci/vendored-gems.gitlab-ci.yml new file mode 100644 index 00000000000..a39c4307c13 --- /dev/null +++ b/.gitlab/ci/vendored-gems.gitlab-ci.yml @@ -0,0 +1,7 @@ +vendor mail-smtp_pool: + extends: + - .vendor:rules:mail-smtp_pool + needs: [] + trigger: + include: vendor/gems/mail-smtp_pool/.gitlab-ci.yml + strategy: depend diff --git a/Gemfile b/Gemfile index 1ca67840692..759d0cb3047 100644 --- a/Gemfile +++ b/Gemfile @@ -511,6 +511,7 @@ gem 'erubi', '~> 1.9.0' # Monkey-patched in `config/initializers/mail_encoding_patch.rb` # See https://gitlab.com/gitlab-org/gitlab/issues/197386 gem 'mail', '= 2.7.1' +gem 'mail-smtp_pool', '~> 0.1.0', path: 'vendor/gems/mail-smtp_pool', require: false # File encryption gem 'lockbox', '~> 0.6.2' diff --git a/Gemfile.lock b/Gemfile.lock index 724412fa8ed..b587b0f77c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +PATH + remote: vendor/gems/mail-smtp_pool + specs: + mail-smtp_pool (0.1.0) + connection_pool (~> 2.0) + mail (~> 2.7) + PATH remote: vendor/shims/mimemagic specs: @@ -1483,6 +1490,7 @@ DEPENDENCIES loofah (~> 2.2) lru_redux mail (= 2.7.1) + mail-smtp_pool (~> 0.1.0)! marginalia (~> 1.10.0) memory_profiler (~> 0.9) method_source (~> 1.0) diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue index be7c0b68b4c..e520b9b8b92 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue @@ -7,6 +7,7 @@ import { GlFormCombobox, GlFormGroup, GlFormSelect, + GlFormInput, GlFormTextarea, GlIcon, GlLink, @@ -41,6 +42,7 @@ export default { GlFormCombobox, GlFormGroup, GlFormSelect, + GlFormInput, GlFormTextarea, GlIcon, GlLink, @@ -128,6 +130,12 @@ export default { return true; }, + scopedVariablesEnabled() { + return !this.isGroup || this.glFeatures.scopedGroupVariables; + }, + scopedVariablesAvailable() { + return !this.isGroup || this.glFeatures.groupScopedCiVariables; + }, variableValidationFeedback() { return `${this.tokenValidationFeedback} ${this.maskedFeedback}`; }, @@ -226,24 +234,27 @@ export default { :label="__('Type')" label-for="ci-variable-type" class="w-50 gl-mr-5" - :class="{ 'w-100': isGroup }" + :class="{ 'w-100': !scopedVariablesEnabled }" > + + diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue index c9943052356..c5f47cf8e84 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue @@ -2,6 +2,7 @@ import { GlTable, GlButton, GlModalDirective, GlIcon } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; import { s__, __ } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { ADD_CI_VARIABLE_MODAL_ID } from '../constants'; import CiVariablePopover from './ci_variable_popover.vue'; @@ -59,6 +60,7 @@ export default { directives: { GlModalDirective, }, + mixins: [glFeatureFlagsMixin()], computed: { ...mapState(['variables', 'valuesHidden', 'isGroup', 'isLoading', 'isDeleting']), valuesButtonText() { @@ -68,7 +70,7 @@ export default { return this.variables && this.variables.length > 0; }, fields() { - if (this.isGroup) { + if (this.isGroup && !this.glFeatures.scopedGroupVariables) { return this.$options.fields.filter((field) => field.key !== 'environment_scope'); } return this.$options.fields; diff --git a/app/assets/javascripts/contributors/index.js b/app/assets/javascripts/contributors/index.js index b6063589734..f66133a074d 100644 --- a/app/assets/javascripts/contributors/index.js +++ b/app/assets/javascripts/contributors/index.js @@ -1,12 +1,15 @@ import Vue from 'vue'; import ContributorsGraphs from './components/contributors.vue'; -import store from './stores'; +import { createStore } from './stores'; export default () => { const el = document.querySelector('.js-contributors-graph'); if (!el) return null; + const { projectGraphPath, projectBranch, defaultBranch } = el.dataset; + const store = createStore(defaultBranch); + return new Vue({ el, store, @@ -14,8 +17,8 @@ export default () => { render(createElement) { return createElement(ContributorsGraphs, { props: { - endpoint: el.dataset.projectGraphPath, - branch: el.dataset.projectBranch, + endpoint: projectGraphPath, + branch: projectBranch, }, }); }, diff --git a/app/assets/javascripts/contributors/stores/index.js b/app/assets/javascripts/contributors/stores/index.js index 38259f46d4c..a4d0004cee5 100644 --- a/app/assets/javascripts/contributors/stores/index.js +++ b/app/assets/javascripts/contributors/stores/index.js @@ -7,12 +7,12 @@ import state from './state'; Vue.use(Vuex); -export const createStore = () => +export const createStore = (defaultBranch) => new Vuex.Store({ actions, mutations, getters, - state: state(), + state: state(defaultBranch), }); -export default createStore(); +export default createStore; diff --git a/app/assets/javascripts/contributors/stores/state.js b/app/assets/javascripts/contributors/stores/state.js index 1dc1a3c7b75..9c6b993e5cb 100644 --- a/app/assets/javascripts/contributors/stores/state.js +++ b/app/assets/javascripts/contributors/stores/state.js @@ -1,5 +1,5 @@ -export default () => ({ +export default (branch) => ({ loading: false, chartData: null, - branch: 'master', + branch, }); diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue index 7f2064b3167..75fb7bbc5c5 100644 --- a/app/assets/javascripts/performance_bar/components/request_selector.vue +++ b/app/assets/javascripts/performance_bar/components/request_selector.vue @@ -58,9 +58,13 @@ export default { (!) - + - + diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue index c74257b176a..7fe6b088ebb 100644 --- a/app/assets/javascripts/performance_bar/components/request_warning.vue +++ b/app/assets/javascripts/performance_bar/components/request_warning.vue @@ -35,8 +35,8 @@ export default { }; diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index e79d43dba93..3e1413da615 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -10,6 +10,8 @@ module Groups before_action :authorize_admin_group! before_action :authorize_update_max_artifacts_size!, only: [:update] before_action :define_variables, only: [:show] + before_action :push_feature_flags, only: [:show] + before_action :push_licensed_features, only: [:show] feature_category :continuous_integration @@ -91,6 +93,16 @@ module Groups def update_group_params params.require(:group).permit(:max_artifacts_size) end + + def push_feature_flags + push_frontend_feature_flag(:scoped_group_variables, group) + end + + # Overridden in EE + def push_licensed_features + end end end end + +Groups::Settings::CiCdController.prepend_if_ee('EE::Groups::Settings::CiCdController') diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index a2289b540ec..75bb6975c6e 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -56,3 +56,5 @@ module Groups end end end + +Groups::VariablesController.prepend_if_ee('EE::Groups::VariablesController') diff --git a/app/models/group.rb b/app/models/group.rb index a0f58fd61b7..a065ea8924e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -86,7 +86,7 @@ class Group < Namespace validate :visibility_level_allowed_by_sub_groups validate :visibility_level_allowed_by_parent validate :two_factor_authentication_allowed - validates :variables, nested_attributes_duplicates: true + validates :variables, nested_attributes_duplicates: { scope: :environment_scope } validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } diff --git a/app/serializers/ci/group_variable_entity.rb b/app/serializers/ci/group_variable_entity.rb index e7d0a957082..30c8239541a 100644 --- a/app/serializers/ci/group_variable_entity.rb +++ b/app/serializers/ci/group_variable_entity.rb @@ -2,5 +2,6 @@ module Ci class GroupVariableEntity < Ci::BasicVariableEntity + expose :environment_scope end end diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 27bb5c076a4..1973b23a062 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -5,4 +5,4 @@ = render 'shared/ref_switcher', destination: 'graphs' = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn gl-button btn-default' -.js-contributors-graph{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json), 'data-project-branch': current_ref } +.js-contributors-graph{ class: container_class, data: { project_graph_path: project_graph_path(@project, current_ref, format: :json), project_branch: current_ref, default_branch: @project.default_branch } } diff --git a/changelogs/unreleased/230717-add-mail-smtp-pool-gem.yml b/changelogs/unreleased/230717-add-mail-smtp-pool-gem.yml new file mode 100644 index 00000000000..80787d1864a --- /dev/null +++ b/changelogs/unreleased/230717-add-mail-smtp-pool-gem.yml @@ -0,0 +1,5 @@ +--- +title: Add support for SMTP connection pooling when sending emails +merge_request: 57805 +author: +type: added diff --git a/changelogs/unreleased/pb-popover-improvement.yml b/changelogs/unreleased/pb-popover-improvement.yml new file mode 100644 index 00000000000..572bcd38ac5 --- /dev/null +++ b/changelogs/unreleased/pb-popover-improvement.yml @@ -0,0 +1,5 @@ +--- +title: Update popover placement and cursor on warning icon in PB +merge_request: 58552 +author: Yogi (@yo) +type: changed diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample index babc0a81938..4a50c29143d 100644 --- a/config/initializers/smtp_settings.rb.sample +++ b/config/initializers/smtp_settings.rb.sample @@ -22,3 +22,28 @@ if Rails.env.production? openssl_verify_mode: 'peer' # See ActionMailer documentation for other possible options } end + +# To use an SMTP connection pool, uncomment the following section: +# +# require 'mail/smtp_pool' +# +# ActionMailer::Base.add_delivery_method :smtp_pool, Mail::SMTPPool +# +# if Rails.env.production? +# Rails.application.config.action_mailer.delivery_method = :smtp_pool +# +# ActionMailer::Base.delivery_method = :smtp_pool +# ActionMailer::Base.smtp_pool_settings = { +# pool: Mail::SMTPPool.create_pool( +# pool_size: Gitlab::Runtime.max_threads, +# address: "email.server.com", +# port: 465, +# user_name: "smtp", +# password: "123456", +# domain: "gitlab.company.com", +# authentication: :login, +# enable_starttls_auto: true, +# openssl_verify_mode: 'peer' # See ActionMailer documentation for other possible options +# ) +# } +# end diff --git a/doc/api/usage_data.md b/doc/api/usage_data.md index f54a0808d51..671e60be587 100644 --- a/doc/api/usage_data.md +++ b/doc/api/usage_data.md @@ -20,7 +20,7 @@ Return all of the raw SQL queries used to compute usage ping. Example request: ```shell -curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/usage_data/queries +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/usage_data/queries" ``` Sample response @@ -66,3 +66,51 @@ Sample response "ci_runners": "SELECT COUNT(\"ci_runners\".\"id\") FROM \"ci_runners\"", ... ``` + +## UsageDataNonSqlMetrics API + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57050) in GitLab 13.11. +> - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default. + +Return all non-SQL metrics data used in the usage ping. + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/usage_data/non_sql_metrics" +``` + +Sample response: + +```json +{ + "recorded_at": "2021-03-26T07:04:03.724Z", + "uuid": null, + "hostname": "localhost", + "version": "13.11.0-pre", + "installation_type": "gitlab-development-kit", + "active_user_count": -3, + "edition": "EE", + "license_md5": "bb8cd0d8a6d9569ff3f70b8927a1f949", + "license_id": null, + "historical_max_users": 0, + "licensee": { + "Name": "John Doe1" + }, + "license_user_count": null, + "license_starts_at": "1970-01-01", + "license_expires_at": "2022-02-26", + "license_plan": "starter", + "license_add_ons": { + "GitLab_FileLocks": 1, + "GitLab_Auditor_User": 1 + }, + "license_trial": null, + "license_subscription_id": "0000", + "license": {}, + "settings": { + "ldap_encrypted_secrets_enabled": false, + "operating_system": "mac_os_x-11.2.2" + }, +... +``` diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js index 991dc8592e9..fbc0bf1323a 100644 --- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js @@ -1,6 +1,7 @@ -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlFormInput } from '@gitlab/ui'; import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; import Vuex from 'vuex'; +import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue'; import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants'; import createStore from '~/ci_variable_list/store'; @@ -15,7 +16,7 @@ describe('Ci variable modal', () => { let store; const createComponent = (method, options = {}) => { - store = createStore(); + store = createStore({ isGroup: options.isGroup }); wrapper = method(CiVariableModal, { attachTo: document.body, stubs: { @@ -27,6 +28,7 @@ describe('Ci variable modal', () => { }); }; + const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown); const findModal = () => wrapper.find(ModalStub); const findAddorUpdateButton = () => findModal() @@ -149,6 +151,61 @@ describe('Ci variable modal', () => { }); }); + describe('Environment scope', () => { + describe('group level variables', () => { + it('renders the environment dropdown', () => { + createComponent(shallowMount, { + isGroup: true, + provide: { + glFeatures: { + scopedGroupVariables: true, + groupScopedCiVariables: true, + }, + }, + }); + + expect(findCiEnvironmentsDropdown().exists()).toBe(true); + expect(findCiEnvironmentsDropdown().isVisible()).toBe(true); + }); + + describe('feature flag is disabled', () => { + it('hides the dropdown', () => { + createComponent(shallowMount, { + isGroup: true, + provide: { + glFeatures: { + scopedGroupVariables: false, + groupScopedCiVariables: true, + }, + }, + }); + + expect(findCiEnvironmentsDropdown().exists()).toBe(false); + }); + }); + + describe('licensed feature is not available', () => { + it('disables the dropdown', () => { + createComponent(mount, { + isGroup: true, + provide: { + glFeatures: { + scopedGroupVariables: true, + groupScopedCiVariables: false, + }, + }, + }); + + const environmentScopeInput = wrapper + .find('[data-testid="environment-scope"]') + .find(GlFormInput); + expect(findCiEnvironmentsDropdown().exists()).toBe(false); + expect(environmentScopeInput.attributes('readonly')).toBe('readonly'); + }); + }); + }); + }); + describe('Validations', () => { const maskError = 'This variable can not be masked.'; diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js index ade2d65b857..abdf14a419a 100644 --- a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js @@ -1,4 +1,3 @@ -import { GlTable } from '@gitlab/ui'; import { createLocalVue, mount } from '@vue/test-utils'; import Vuex from 'vuex'; import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue'; @@ -12,21 +11,24 @@ describe('Ci variable table', () => { let wrapper; let store; - const createComponent = () => { - store = createStore(); - store.state.isGroup = true; + const createComponent = (isGroup = false, scopedGroupVariables = false) => { + store = createStore({ isGroup }); jest.spyOn(store, 'dispatch').mockImplementation(); wrapper = mount(CiVariableTable, { attachTo: document.body, localVue, store, + provide: { + glFeatures: { + scopedGroupVariables, + }, + }, }); }; const findRevealButton = () => wrapper.find({ ref: 'secret-value-reveal-button' }); const findEditButton = () => wrapper.find({ ref: 'edit-ci-variable' }); const findEmptyVariablesPlaceholder = () => wrapper.find({ ref: 'empty-variables' }); - const findTable = () => wrapper.find(GlTable); beforeEach(() => { createComponent(); @@ -40,12 +42,14 @@ describe('Ci variable table', () => { expect(store.dispatch).toHaveBeenCalledWith('fetchVariables'); }); - it('fields prop does not contain environment_scope if group', () => { - expect(findTable().props('fields')).not.toEqual( + it('fields do not contain environment_scope if group level and feature is disabled', () => { + createComponent(true, false); + + expect(wrapper.vm.fields).not.toEqual( expect.arrayContaining([ expect.objectContaining({ key: 'environment_scope', - label: 'Environment Scope', + label: 'Environments', }), ]), ); diff --git a/spec/serializers/ci/group_variable_entity_spec.rb b/spec/serializers/ci/group_variable_entity_spec.rb index a7e12905924..9b64e263992 100644 --- a/spec/serializers/ci/group_variable_entity_spec.rb +++ b/spec/serializers/ci/group_variable_entity_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Ci::GroupVariableEntity do subject { entity.as_json } it 'contains required fields' do - expect(subject).to include(:id, :key, :value, :protected, :variable_type) + expect(subject).to include(:id, :key, :value, :protected, :variable_type, :environment_scope) end end end diff --git a/vendor/gems/mail-smtp_pool/.gitignore b/vendor/gems/mail-smtp_pool/.gitignore new file mode 100644 index 00000000000..1fbdf80cd36 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/.gitignore @@ -0,0 +1,3 @@ +Gemfile.lock +*.gem +.bundle diff --git a/vendor/gems/mail-smtp_pool/.gitlab-ci.yml b/vendor/gems/mail-smtp_pool/.gitlab-ci.yml new file mode 100644 index 00000000000..56eff5b30a7 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/.gitlab-ci.yml @@ -0,0 +1,29 @@ +workflow: + rules: + - if: $CI_MERGE_REQUEST_ID + +.rspec: + cache: + key: mail-smtp_pool-ruby + paths: + - vendor/gems/mail-smtp_pool/vendor/ruby + before_script: + - cd vendor/gems/mail-smtp_pool + - ruby -v # Print out ruby version for debugging + - gem install bundler --no-document # Bundler is not installed with the image + - bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby + - bundle install -j $(nproc) + script: + - bundle exec rspec + +rspec-2.6: + image: "ruby:2.6" + extends: .rspec + +rspec-2.7: + image: "ruby:2.7" + extends: .rspec + +rspec-3.0: + image: "ruby:3.0" + extends: .rspec diff --git a/vendor/gems/mail-smtp_pool/Gemfile b/vendor/gems/mail-smtp_pool/Gemfile new file mode 100644 index 00000000000..7f4f5e950d1 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gemspec diff --git a/vendor/gems/mail-smtp_pool/LICENSE b/vendor/gems/mail-smtp_pool/LICENSE new file mode 100644 index 00000000000..e6de2f90864 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-2021 GitLab B.V. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gems/mail-smtp_pool/README.md b/vendor/gems/mail-smtp_pool/README.md new file mode 100644 index 00000000000..bdb2be97663 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/README.md @@ -0,0 +1,57 @@ +# Mail::SMTPPool + +This gem is an extension to `Mail` that allows delivery of emails using an SMTP connection pool + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'mail-smtp_pool' +``` + +And then execute: + +```shell +bundle +``` + +Or install it yourself as: + +```shell +gem install mail-smtp_pool +``` + +## Usage with ActionMailer + +```ruby +# config/environments/development.rb + +Rails.application.configure do + ... + + ActionMailer::Base.add_delivery_method :smtp_pool, Mail::SMTPPool + + config.action_mailer.perform_deliveries = true + config.action_mailer.smtp_pool_settings = { + pool: Mail::SMTPPool.create_pool( + pool_size: 5, + pool_timeout: 5, + address: 'smtp.gmail.com', + port: 587, + domain: 'example.com', + user_name: '', + password: '', + authentication: 'plain', + enable_starttls_auto: true + ) + } +end +``` + +Configuration options: + +* `pool_size` - The maximum number of SMTP connections in the pool. Connections are created lazily as needed. +* `pool_timeout` - The number of seconds to wait for a connection in the pool to be available. A `Timeout::Error` exception is raised when this is exceeded. + +This also accepts all options supported by `Mail::SMTP`. See https://www.rubydoc.info/gems/mail/2.6.1/Mail/SMTP for more information. diff --git a/vendor/gems/mail-smtp_pool/lib/mail/smtp_pool.rb b/vendor/gems/mail-smtp_pool/lib/mail/smtp_pool.rb new file mode 100644 index 00000000000..ab8a7652058 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/lib/mail/smtp_pool.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'connection_pool' +require 'mail/smtp_pool/connection' + +module Mail + class SMTPPool + POOL_DEFAULTS = { + pool_size: 5, + pool_timeout: 5 + }.freeze + + class << self + def create_pool(settings = {}) + pool_settings = POOL_DEFAULTS.merge(settings) + smtp_settings = settings.reject { |k, v| POOL_DEFAULTS.keys.include?(k) } + + ConnectionPool.new(size: pool_settings[:pool_size], timeout: pool_settings[:pool_timeout]) do + Mail::SMTPPool::Connection.new(smtp_settings) + end + end + end + + def initialize(settings) + raise ArgumentError, 'pool is required. You can create one using Mail::SMTPPool.create_pool.' if settings[:pool].nil? + + @pool = settings[:pool] + end + + def deliver!(mail) + @pool.with { |conn| conn.deliver!(mail) } + end + end +end diff --git a/vendor/gems/mail-smtp_pool/lib/mail/smtp_pool/connection.rb b/vendor/gems/mail-smtp_pool/lib/mail/smtp_pool/connection.rb new file mode 100644 index 00000000000..ab0d20153d8 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/lib/mail/smtp_pool/connection.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# A connection object that can be used to deliver mail. +# +# This is meant to be used in a pool so the main difference between this +# and Mail::SMTP is that this expects deliver! to be called multiple times. +# +# SMTP connection reset and error handling is handled by this class and +# the SMTP connection is not closed after a delivery. + +require 'mail' + +module Mail + class SMTPPool + class Connection < Mail::SMTP + def initialize(values) + super + + @smtp_session = nil + end + + def deliver!(mail) + response = Mail::SMTPConnection.new(connection: smtp_session, return_response: true).deliver!(mail) + + settings[:return_response] ? response : self + end + + def finish + finish_smtp_session if @smtp_session && @smtp_session.started? + end + + private + + def smtp_session + return start_smtp_session if @smtp_session.nil? || !@smtp_session.started? + return @smtp_session if reset_smtp_session + + finish_smtp_session + start_smtp_session + end + + def start_smtp_session + @smtp_session = build_smtp_session.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication]) + end + + def reset_smtp_session + !@smtp_session.instance_variable_get(:@error_occurred) && @smtp_session.rset.success? + rescue Net::SMTPError, IOError + false + end + + def finish_smtp_session + @smtp_session.finish + rescue Net::SMTPError, IOError + ensure + @smtp_session = nil + end + end + end +end diff --git a/vendor/gems/mail-smtp_pool/mail-smtp_pool.gemspec b/vendor/gems/mail-smtp_pool/mail-smtp_pool.gemspec new file mode 100644 index 00000000000..3d9036f19b1 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/mail-smtp_pool.gemspec @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) + +Gem::Specification.new do |spec| + spec.name = 'mail-smtp_pool' + spec.version = '0.1.0' + spec.authors = ['Heinrich Lee Yu'] + spec.email = ['heinrich@gitlab.com'] + + spec.summary = 'Mail extension for sending using an SMTP connection pool' + spec.homepage = 'https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/mail-smtp_pool' + spec.metadata = { 'source_code_uri' => 'https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/mail-smtp_pool' } + spec.license = 'MIT' + + spec.files = Dir['lib/**/*.rb'] + spec.require_paths = ['lib'] + + # Please maintain alphabetical order for dependencies + spec.add_runtime_dependency 'connection_pool', '~> 2.0' + spec.add_runtime_dependency 'mail', '~> 2.7' + + # Please maintain alphabetical order for dev dependencies + spec.add_development_dependency 'rspec', '~> 3.10.0' +end diff --git a/vendor/gems/mail-smtp_pool/spec/lib/mail/smtp_pool/connection_spec.rb b/vendor/gems/mail-smtp_pool/spec/lib/mail/smtp_pool/connection_spec.rb new file mode 100644 index 00000000000..78426296406 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/spec/lib/mail/smtp_pool/connection_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mail::SMTPPool::Connection do + let(:connection) { described_class.new({}) } + let(:mail) do + Mail.new do + from 'mikel@test.lindsaar.net' + to 'you@test.lindsaar.net' + subject 'This is a test email' + body 'Test body' + end + end + + after do + MockSMTP.clear_deliveries + end + + describe '#deliver!' do + it 'delivers mail using the same SMTP connection' do + mock_smtp = MockSMTP.new + + expect(Net::SMTP).to receive(:new).once.and_return(mock_smtp) + expect(mock_smtp).to receive(:sendmail).twice.and_call_original + expect(mock_smtp).to receive(:rset).once.and_call_original + + connection.deliver!(mail) + connection.deliver!(mail) + + expect(MockSMTP.deliveries.size).to eq(2) + end + + context 'when RSET fails' do + let(:mock_smtp) { MockSMTP.new } + let(:mock_smtp_2) { MockSMTP.new } + + before do + expect(Net::SMTP).to receive(:new).twice.and_return(mock_smtp, mock_smtp_2) + end + + context 'with an IOError' do + before do + expect(mock_smtp).to receive(:rset).once.and_raise(IOError) + end + + it 'creates a new SMTP connection' do + expect(mock_smtp).to receive(:sendmail).once.and_call_original + expect(mock_smtp).to receive(:finish).once.and_call_original + expect(mock_smtp_2).to receive(:sendmail).once.and_call_original + + connection.deliver!(mail) + connection.deliver!(mail) + + expect(MockSMTP.deliveries.size).to eq(2) + end + end + + context 'with an SMTP error' do + before do + expect(mock_smtp).to receive(:rset).once.and_raise(Net::SMTPServerBusy) + end + + it 'creates a new SMTP connection' do + expect(mock_smtp).to receive(:sendmail).once.and_call_original + expect(mock_smtp).to receive(:finish).once.and_call_original + expect(mock_smtp_2).to receive(:sendmail).once.and_call_original + + connection.deliver!(mail) + connection.deliver!(mail) + + expect(MockSMTP.deliveries.size).to eq(2) + end + + context 'and closing the old connection fails' do + before do + expect(mock_smtp).to receive(:finish).once.and_raise(IOError) + end + + it 'creates a new SMTP connection' do + expect(mock_smtp).to receive(:sendmail).once.and_call_original + expect(mock_smtp_2).to receive(:sendmail).once.and_call_original + + connection.deliver!(mail) + connection.deliver!(mail) + + expect(MockSMTP.deliveries.size).to eq(2) + end + end + end + end + end +end diff --git a/vendor/gems/mail-smtp_pool/spec/lib/mail/smtp_pool_spec.rb b/vendor/gems/mail-smtp_pool/spec/lib/mail/smtp_pool_spec.rb new file mode 100644 index 00000000000..aa2a0e19ac8 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/spec/lib/mail/smtp_pool_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mail::SMTPPool do + describe '.create_pool' do + it 'sets the default pool settings' do + expect(ConnectionPool).to receive(:new).with(size: 5, timeout: 5).once + + described_class.create_pool + end + + it 'allows overriding pool size and timeout' do + expect(ConnectionPool).to receive(:new).with(size: 3, timeout: 2).once + + described_class.create_pool(pool_size: 3, pool_timeout: 2) + end + + it 'creates an SMTP connection with the correct settings' do + settings = { address: 'smtp.example.com', port: '465' } + + smtp_pool = described_class.create_pool(settings) + + expect(Mail::SMTPPool::Connection).to receive(:new).with(settings).once.and_call_original + + smtp_pool.checkout + end + end + + describe '#initialize' do + it 'raises an error if a pool is not specified' do + expect { described_class.new({}) }.to raise_error( + ArgumentError, 'pool is required. You can create one using Mail::SMTPPool.create_pool.' + ) + end + end + + describe '#deliver!' do + let(:mail) do + Mail.new do + from 'mikel@test.lindsaar.net' + to 'you@test.lindsaar.net' + subject 'This is a test email' + body 'Test body' + end + end + + after do + MockSMTP.clear_deliveries + end + + it 'delivers mail using a connection from the pool' do + connection_pool = double(ConnectionPool) + connection = double(Mail::SMTPPool::Connection) + + expect(connection_pool).to receive(:with).and_yield(connection) + expect(connection).to receive(:deliver!).with(mail) + + described_class.new(pool: connection_pool).deliver!(mail) + end + + it 'delivers mail' do + described_class.new(pool: described_class.create_pool).deliver!(mail) + + expect(MockSMTP.deliveries.size).to eq(1) + end + end +end diff --git a/vendor/gems/mail-smtp_pool/spec/spec_helper.rb b/vendor/gems/mail-smtp_pool/spec/spec_helper.rb new file mode 100644 index 00000000000..4d339850381 --- /dev/null +++ b/vendor/gems/mail-smtp_pool/spec/spec_helper.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'mail/smtp_pool' + +# Original mockup from ActionMailer +# Based on https://github.com/mikel/mail/blob/22a7afc23f253319965bf9228a0a430eec94e06d/spec/spec_helper.rb#L74-L138 +class MockSMTP + def self.deliveries + @@deliveries + end + + def self.security + @@security + end + + def initialize + @@deliveries = [] + @@security = nil + @started = false + end + + def sendmail(mail, from, to) + @@deliveries << [mail, from, to] + 'OK' + end + + def rset + Net::SMTP::Response.parse('250 OK') + end + + def start(*args) + @started = true + + if block_given? + result = yield(self) + @started = false + + return result + else + return self + end + end + + def started? + @started + end + + def finish + @started = false + return true + end + + def self.clear_deliveries + @@deliveries = [] + end + + def self.clear_security + @@security = nil + end + + def enable_tls(context) + raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @@security && @@security != :enable_tls + @@security = :enable_tls + context + end + + def enable_starttls(context = nil) + raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @@security == :enable_tls + @@security = :enable_starttls + context + end + + def enable_starttls_auto(context) + raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @@security == :enable_tls + @@security = :enable_starttls_auto + context + end +end + +class Net::SMTP + def self.new(*args) + MockSMTP.new + end +end