schleuder-gitlab-ticketing 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []