schleuder-gitlab-ticketing 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b59177cf964bf88408cc0d315ba893e8c5a8379fc250b1df318f36c136d1a2b0
4
+ data.tar.gz: 7b407e3792310e646fe68c949eb7d1b71d15bd617b89e2c85178a29492046cab
5
+ SHA512:
6
+ metadata.gz: 1b448ce81e1cac8ae9ddeb6fa66d7517bc927fa5e3bafa283caa51fee703aca240870848aa6d205b6a2171614834780e250997770f1f2f90e52885e88270e2d6
7
+ data.tar.gz: 26164f54b6bca5e1fdfd5abb23208e87281028e4c5798279b2bdb98da3a8df7b3e391b9ed9e7fb6fc57d630cab243be3b629d32d45e6b42593f983ebac5ad7e8
@@ -0,0 +1,191 @@
1
+ Schleuder Gitlab Ticketing
2
+ ==========================
3
+
4
+ A schleuder filter/plugin to hook lists into the issue tracker of a gitlab project.
5
+
6
+ Background
7
+ ----------
8
+
9
+ Schleuderlists are not only a helpful tool for communication within groups, they can also be
10
+ used for newsletters, or by using its famous remailer capabilities to be used as a contact
11
+ address for a project or as a help desk.
12
+
13
+ In the latter two cases keeping an overview of the different interactions can become somewhat
14
+ cumbersome, especially in a bigger collective where not everybody is able to dedicate the same
15
+ amount of time/attention to the contact address / help desk. Hence it is easy that a certain
16
+ thread (and so task) on the list is getting lost. Having some kind of ticketing system in place
17
+ can help to easily keep track of what is done, what is in progress and what should be looked at.
18
+
19
+ Additionally, folks do not like to double the work and hence as much as possible should be done
20
+ while emails flow over the list.
21
+
22
+ This schleuder filter / plugin allows to map emails through an id in the subject to issues in
23
+ a gitlab project. Additionally, it opens an issue on new emails or closes issues if the subject
24
+ indicates that the job is done.
25
+
26
+ The gitlab issues are mainly used to keep the state of a certain email thread and contains only
27
+ as much plaintext information as was transmitted in plaintext over the wire. This means it only
28
+ stores From:, Subject: and Message-Id: as issue comments.
29
+
30
+ How it works
31
+ ------------
32
+
33
+ The filter / plugin hooks into the filter process list in schleuder. First schleuder will check
34
+ whether there is a configuration for the current list, as well as whether this configuration
35
+ is correct. Otherwise the filter is immediately skipped.
36
+
37
+ Schleuder then dispatches the processing to the specific list instance, which checks whether
38
+ the email comes from a sender that shall be ignored (e.g. well known spammers or announce lists)
39
+ or whether the subject matches any of the configured subject filters. The email is also ignored
40
+ if the well known `X-Spam-Flag` header is set to `yes`.
41
+
42
+ If none of that matches, the email is being processed and first the email's subject is analyzed
43
+ to get an already existing ticket id. The plugin expects something like `Subject: my subject [gp#123]`,
44
+ where gp is a configureable project identifier and 123 matches an issue id in gitlab. The brackets
45
+ enclose the ticket identifier and can be anywhere within the subject.
46
+
47
+ If no ticket id can be found or no issue can be found in gitlab a new issue will be created in
48
+ gitlab. The ticket id will be appended to the subject, a faulty ticket id will be replaced.
49
+
50
+ If a ticket can be found, a comment is added to the issue, that includes the sender email address,
51
+ as well as the Subject and Message-ID of the email.
52
+
53
+ Additionally, if the email is sent from an email address, that is a member of the gitlab project
54
+ the ticket will be assigned to said user. Furthermore, the label `inprocess` is added to ticket
55
+ to indicate that someone of the project is working on that thread.
56
+
57
+ If the ticket is already closed it will be re-opened.
58
+
59
+ If the subject contains: `[ok]` the ticket will be closed and the label `inprocess` removed. An
60
+ already closed ticket stays closed.
61
+
62
+ That's it.
63
+
64
+ Prerequisits
65
+ ------------
66
+
67
+ * ruby >=2.1
68
+ * schleuder >= 3.3.0
69
+ * A working schleuder installation and a schleuder list
70
+ * A gitlab project and a dedicated user and its API token
71
+
72
+ Installation
73
+ ------------
74
+
75
+ * Install the gem (depends on the gitlab gem)
76
+ * Link `lib/schleuder/filters/post_decrpytion/99_gitlab_ticketing.rb` into `/usr/local/lib/schleuder/filters/post_decryption/`,
77
+ so the plugin gets loaded.
78
+
79
+ Configuration
80
+ -------------
81
+
82
+ The plugin is looking for a configuration in `/etc/schleuder/gitlab.yml`. The configuration file
83
+ expects a `gitlab` and a lists `configuration` section:
84
+
85
+ ```
86
+ gitlab:
87
+ endpoint: https://gitlab.example.com/api/v4
88
+ token: xxxx
89
+
90
+ lists:
91
+ test@schleuder.example.com:
92
+ project: tickets
93
+ namespace: support
94
+ ```
95
+
96
+ The key of the lists configuration indicates the listname on schleuder side, which matches its
97
+ email address. A list's setting requires 2 configuration options: `project` & `namespace` which
98
+ map to the path within gitlab, where the project exists. Namespace can either be the group or
99
+ the users name, depending of where a project lives.
100
+
101
+ The plugin supports working for multiple lists, as well as allows individual gitlab configuration
102
+ for different lists:
103
+
104
+ ```
105
+ gitlab:
106
+ endpoint: https://gitlab.example.com/api/v4
107
+ token: xxxx
108
+ lists:
109
+ test@schleuder.example.com:
110
+ project: tickets
111
+ namespace: support
112
+ test2@schleuder.example.com:
113
+ gitlab:
114
+ endpoint: https://gitlab2.example.com/api/v4
115
+ token: aaaa
116
+ ```
117
+
118
+ Additionally, you can specify filters based on the sender or the subject. They must be valid ruby
119
+ regexps and can also be specified on a global and local level.
120
+
121
+ ```
122
+ gitlab:
123
+ endpoint: https://gitlab.example.com/api/v4
124
+ token: xxxx
125
+
126
+ subject_filters:
127
+ - '\[announce\]'
128
+ sender_filters:
129
+ - '^spammer@example\.com$'
130
+
131
+ lists:
132
+ test@schleuder.example.com:
133
+ project: tickets
134
+ namespace: support
135
+ subject_filters:
136
+ - 'ignore me'
137
+ test2@schleuder.example.com:
138
+ gitlab:
139
+ endpoint: https://gitlab2.example.com/api/v4
140
+ token: aaaa
141
+ sender_filters:
142
+ - 'noreply@example\.com'
143
+
144
+ ```
145
+
146
+ Additionally, you can specify a specifc identifier to more easily identify a particular ticket id in
147
+ the subject:
148
+
149
+ ```
150
+ lists:
151
+ test@schleuder.example.com:
152
+ project: tickets
153
+ namespace: support
154
+ ticket_prefix: ourid
155
+ ```
156
+
157
+ This will look for the following string in a subject: `[ourid#\d+]`. By default it would look for:
158
+ `[gl/test#\d+]`
159
+
160
+ Todo
161
+ ----
162
+
163
+ This plugin is in a working state and has been tested by multiple use cases for nearly a year.
164
+
165
+ Some more ideas:
166
+
167
+ * Use schleuder's verification capabilities to further prove that the email comes from a list member.
168
+ * Hook into schleuder's plugin capabilities, to allow more ticket actions (e.g. assign, labels, ...)
169
+
170
+ Contributing
171
+ ------------
172
+
173
+ Please see [CONTRIBUTING.md](CONTRIBUTING.md).
174
+
175
+
176
+ Mission statement
177
+ -----------------
178
+
179
+ Please see [MISSION_STATEMENT.md](MISSION_STATEMENT.md).
180
+
181
+
182
+ Code of Conduct
183
+ ---------------
184
+
185
+ We adopted a code of conduct. Please read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
186
+
187
+
188
+ License
189
+ -------
190
+
191
+ GNU GPL 3.0. Please see [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,6 @@
1
+
2
+ module SchleuderGitlabTicketing
3
+ end
4
+ require 'schleuder-gitlab-ticketing/gitlab_config'
5
+ require 'schleuder-gitlab-ticketing/config'
6
+ require 'schleuder-gitlab-ticketing/list'
@@ -0,0 +1,45 @@
1
+ module SchleuderGitlabTicketing
2
+ class Config
3
+ include SchleuderGitlabTicketing::GitlabConfig
4
+
5
+ def initialize(config_path='/etc/schleuder/gitlab.yml')
6
+ @config = if File.exists?(config_path)
7
+ YAML.load_file(config_path)
8
+ else
9
+ {}
10
+ end
11
+ end
12
+
13
+ # returns true if mail was handled
14
+ # returns 'config-error' if list-config is not properly
15
+ # returns nil if list is not configured to handle
16
+ # gitlab plugin
17
+ def process_list(list_name, mail)
18
+ if l = lists[list_name]
19
+ if l.configured?
20
+ l.process(mail)
21
+ else
22
+ 'config-error'
23
+ end
24
+ else
25
+ nil
26
+ end
27
+ end
28
+
29
+ def lists
30
+ @lists ||= read_lists
31
+ end
32
+
33
+ private
34
+ def read_lists
35
+ Hash(@config['lists']).inject({}) do |res,a|
36
+ n,v = a
37
+ v['gitlab'] ||= gitlab
38
+ v['subject_filters'] = Array(v['subject_filters']) + Array(@config['subject_filters'])
39
+ v['sender_filters'] = Array(v['sender_filters']) + Array(@config['sender_filters'])
40
+ res[n] = SchleuderGitlabTicketing::List.new(n,v)
41
+ res
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ module SchleuderGitlabTicketing
2
+ module GitlabConfig
3
+ def gitlab
4
+ @gitlab ||= if @config['gitlab']['endpoint'] && @config['gitlab']['token']
5
+ Gitlab.client(endpoint: @config['gitlab']['endpoint'], private_token: @config['gitlab']['token'])
6
+ else
7
+ nil
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,198 @@
1
+ require 'yaml'
2
+ require 'gitlab'
3
+ require 'set'
4
+ module SchleuderGitlabTicketing
5
+ class List
6
+ include SchleuderGitlabTicketing::GitlabConfig
7
+
8
+ def initialize(name, config)
9
+ @name = name
10
+ @config = config
11
+ end
12
+
13
+ def configured?
14
+ unless @config['project'] && @config['namespace'] && @config['gitlab'] && gitlab && project
15
+ return false
16
+ end
17
+ true
18
+ end
19
+
20
+ def process(mail)
21
+ return false if ignore_mail?(mail)
22
+ ticket_id = get_ticket_id(mail.subject)
23
+ ticket = ticket_id ? get_ticket(ticket_id) : nil
24
+
25
+ if (title = clean_subject(mail.subject)).nil? || title.empty?
26
+ title = "Request from #{mail.from.first}"
27
+ end
28
+ desc = desc(mail)
29
+ updates = {}
30
+ labels = Set.new
31
+
32
+ if !ticket
33
+ ticket = create_ticket(title, desc)
34
+ ticket_id = ticket.iid
35
+ mail.subject = update_subject(mail.subject, ticket_id)
36
+ else
37
+ labels = Set.new(ticket.labels)
38
+ comment_ticket(ticket_id,desc)
39
+ assignee_id = team_member_id(mail.from.first)
40
+ if assignee_id && ((as = ticket.assignee).nil? || as.id != assignee_id)
41
+ updates[:assignee_id] = assignee_id
42
+ end
43
+ end
44
+
45
+ bc = be_closed?(mail.subject)
46
+ tc = ticket_closed?(ticket)
47
+
48
+ if !tc && bc
49
+ labels.delete('inprocess')
50
+ updates[:state_event] = 'close'
51
+ elsif !tc && !bc
52
+ labels << 'inprocess' if updates[:assignee_id]
53
+ elsif tc && !bc
54
+ labels << 'inprocess'
55
+ updates[:state_event] = 'reopen'
56
+ end
57
+ if labels.empty? && (updates[:state_event] == 'close')
58
+ # make sure we remove the inprocess label
59
+ updates[:labels] = ''
60
+ elsif !labels.empty? && (labels.to_a.sort != ticket.labels.sort)
61
+ updates[:labels] = labels.to_a.join(',')
62
+ end
63
+ update_ticket(ticket_id, updates)
64
+ true
65
+ end
66
+
67
+ def gitlab
68
+ @gitlab ||= if @config['gitlab'].is_a?(Gitlab::Client)
69
+ @config['gitlab']
70
+ else
71
+ super
72
+ end
73
+ end
74
+
75
+ def sender_filters
76
+ Array(@config['sender_filters'])
77
+ end
78
+ def subject_filters
79
+ Array(@config['subject_filters'])
80
+ end
81
+
82
+ private
83
+ def desc(mail)
84
+ res = []
85
+ res << "From: #{mail.from.first}"
86
+ res << "Message-Id: #{mail.message_id}"
87
+ res << "Subject: #{mail.subject || '[not set]'}"
88
+ res.join("\n\n")
89
+ end
90
+
91
+ def be_closed?(subject)
92
+ !(subject =~ /\[ok\]/).nil?
93
+ end
94
+
95
+ def ticket_closed?(ticket)
96
+ ticket.state == 'closed'
97
+ end
98
+
99
+ def update_ticket(ticket_id,updates)
100
+ return if updates.empty?
101
+ gitlab.edit_issue(project.id, ticket_id, updates)
102
+ rescue Gitlab::Error::NotFound => e
103
+ nil
104
+ end
105
+
106
+ def create_ticket(title, desc)
107
+ opts = { description: desc }
108
+ gitlab.create_issue(project.id, title, opts)
109
+ end
110
+
111
+ def comment_ticket(ticket_id, desc)
112
+ gitlab.create_issue_note(project.id, ticket_id, desc)
113
+ end
114
+
115
+ def get_ticket(id)
116
+ gitlab.issue(project.id, id)
117
+ rescue Gitlab::Error::NotFound => e
118
+ nil
119
+ end
120
+
121
+ def get_ticket_id(str)
122
+ if str && (m = str.match(/.*\[#{Regexp.escape(ticket_prefix)}\#(\d+)\].*/)) && m[1]
123
+ m[1].to_i
124
+ else
125
+ nil
126
+ end
127
+ end
128
+
129
+ def project
130
+ @project ||= gitlab.search_projects(@config['project']).find{|p| p.namespace.name == @config['namespace'] && p.name == @config['project'] }
131
+ rescue Gitlab::Error::NotFound => e
132
+ nil
133
+ end
134
+
135
+ def issues
136
+ gitlab.issues(p.id)
137
+ end
138
+
139
+ def team_member_id(email)
140
+ if user_id = find_user_id(email)
141
+ user_id if project_member?(project, user_id)
142
+ else
143
+ nil
144
+ end
145
+ rescue Gitlab::Error::NotFound => e
146
+ nil
147
+ end
148
+
149
+ def project_member?(project, user_id)
150
+ begin
151
+ gitlab.team_member(project.id, user_id)
152
+ rescue Gitlab::Error::NotFound
153
+ gitlab.group_member(project.namespace.id, user_id)
154
+ end
155
+ rescue Gitlab::Error::NotFound => e
156
+ nil
157
+ end
158
+
159
+ def find_user_id(username_or_email)
160
+ users = gitlab.user_search(username_or_email)
161
+ # did we look for an email => exact match
162
+ # or by username?
163
+ user = if username_or_email =~ /@/
164
+ users.first
165
+ else
166
+ users.find do |u|
167
+ u.name == username_or_email || u.username == username_or_email
168
+ end
169
+ end
170
+ user.nil? ? nil : user.id
171
+ rescue Gitlab::Error::NotFound => e
172
+ nil
173
+ end
174
+
175
+ def ignore_mail?(mail)
176
+ return true if mail.respond_to?(:[]) && mail['X-Spam-Flag'].to_s.downcase == 'yes'
177
+ return true if sender_filters.any?{|s| mail.from.first =~ /#{s}/ }
178
+ return true if subject_filters.any?{|s| mail.subject =~ /#{s}/ }
179
+ false
180
+ end
181
+
182
+ def ticket_prefix
183
+ @ticket_prefix ||= (@config['ticket_prefix'] || "gl/#{@config['project']}")
184
+ end
185
+
186
+ def clean_subject(subj)
187
+ subj.nil? ? nil : subj.gsub(/^(Re|Fw):\s*/,'').gsub(/\[[<>]?#{list_subj}\]\s*/,'').gsub(/\s*\[#{Regexp.escape(ticket_prefix)}#\d+\](\s*$)?/,'').gsub(/\s*\[ok\](\s*$)?/,'').strip
188
+ end
189
+
190
+ def list_subj
191
+ @list_subj ||= (@config['list_subj'] || @name.split('@',2).first)
192
+ end
193
+
194
+ def update_subject(subject,ticket_id)
195
+ [subject.nil? ? nil : subject.gsub(/(\s)?\[#{Regexp.escape(ticket_prefix)}#\d+\]/,''), "[#{ticket_prefix}##{ticket_id}]"].compact.join(' ')
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,3 @@
1
+ module SchleuderGitlabTicketing
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,25 @@
1
+ module Schleuder
2
+ module Filters
3
+ def self.gitlab_ticketing(list, mail)
4
+ @gt_config ||= begin
5
+ require 'schleuder-gitlab-ticketing'
6
+ SchleuderGitlabTicketing::Config.new
7
+ end
8
+
9
+ res = @gt_config.process_list(list.email, mail)
10
+ if res.nil?
11
+ list.logger.debug('No gitlab ticketing enabled for list')
12
+ elsif res == 'config-error'
13
+ list.logger.error('gitlab ticketing not correctly configured')
14
+ elsif res == false
15
+ list.logger.debug('Email skipped by list configuration')
16
+ end
17
+ nil
18
+ # make sure we catch any error with that filter
19
+ # so we don't affect list processing
20
+ rescue Exception => e
21
+ list.logger.notify_admin "Unable to process the following mail with gitlab-ticketing:\n\n#{e}\n\n#{e.backtrace.join("\n")}", mail.original_message, 'Error while processing mail with gitlab-ticketing'
22
+ nil
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: schleuder-gitlab-ticketing
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - schleuder dev team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-02-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gitlab
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: schleuder
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.5'
55
+ description: Schleuder Gitlab Ticketing combines a schleuder list with the issue tracker
56
+ of a gitlab project and operates as a state tracker of threads on the list. This
57
+ allows one to keep an overview on the state of various requests on a help desk powered
58
+ by schleuder
59
+ email: team@schleuder.org
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - README.md
65
+ - lib/schleuder-gitlab-ticketing.rb
66
+ - lib/schleuder-gitlab-ticketing/config.rb
67
+ - lib/schleuder-gitlab-ticketing/gitlab_config.rb
68
+ - lib/schleuder-gitlab-ticketing/list.rb
69
+ - lib/schleuder-gitlab-ticketing/version.rb
70
+ - lib/schleuder/filters/post_decryption/99_gitlab_ticketing.rb
71
+ homepage: https://schleuder.org/
72
+ licenses:
73
+ - GPL-3.0
74
+ metadata:
75
+ homepage_uri: https://schleuder.org/
76
+ documentation_uri: https://schleuder.org/docs/
77
+ changelog_uri: https://0xacab.org/schleuder/schleuder-gitlab-ticketing/blob/master/CHANGELOG.md
78
+ source_code_uri: https://0xacab.org/schleuder/schleuder-gitlab-ticketing/
79
+ bug_tracker_uri: https://0xacab.org/schleuder/schleuder-gitlab-ticketing/issues
80
+ mailing_list_uri: https://lists.nadir.org/mailman/listinfo/schleuder-announce/
81
+ post_install_message: "\n\n To activate the filter plugin you will need to link\n
82
+ \ lib/schleuder/filters/post_decryption/99_gitlab_ticketing.rb\n to /usr/local/lib/schleuder/filters/post_decryption/\n\n
83
+ \ "
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 2.1.0
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project: "[none]"
99
+ rubygems_version: 2.7.8
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Schleuder Gitlab Ticketing is a filter plugin for schleuder to hook a list
103
+ into a gitlab issue tracker
104
+ test_files: []